GOVERNANCE

Who did what last quarter: separating agent actions from human actions

Actor kind belongs in a column, not buried in a JSON payload. Otherwise the first question a regulator asks becomes a two-week project.

Audit 7 min read Key figure · Agent vs human, one dimension All posts

"How much of last quarter's work was done by agents rather than by people?" is becoming the first question regulators and boards ask about AI deployments. It should be a GROUP BY. In most systems it is a two-week project.

The reason is almost always the same: actor kind was never given a column. It is inferred from a naming convention, extracted from a JSON blob, or reconstructed by joining to a user table that does not distinguish service accounts from staff.

Three kinds, not two

The first mistake is treating this as a boolean. We did, and had to retract a number because of it.

KindWhat it isExampleAccountable party
human A person, deciding Supervisor approves a $58k settlement That person
agent A model-driven actor with a role and authority Adjuster posts a reserve The agent's owner, under its authority limit
system Deterministic automation. No model, no judgement. Nightly job expires stale quotes The service owner

Collapsing system into either of the others produces a wrong answer in a way that is embarrassing to correct. Fold it into agent and you overstate your AI footprint to a regulator. Fold it into human and you understate it, which is worse.

Our retraction was the first kind: a batch job that expired quotes overnight was attributed to agents because it ran under a service account whose name started with the same prefix. It inflated "agent actions" by several thousand rows in a quarter.

A nightly cron is not an agent. If your taxonomy cannot say so, your AI adoption number is wrong in a direction you will have to explain.

Set it at the edge, once

The value has to be established where the request enters, from the credential, not decided later by whichever service happens to write the row.

flowchart LR
    REQ["Inbound request"] --> MW["Identity middleware
resolves actor_code
and actor_kind"] MW --> CTX["Request context"] CTX --> SVC["Service does its work"] SVC --> AC["after_commit hook"] AC --> BE["business_event
actor_code, actor_kind"] MW -.->|"on behalf of"| OBO["on_behalf_of
set when an agent
acts for a named person"] OBO --> BE style MW fill:#efecfe,stroke:#4b34e0 style AC fill:#e2f5f9,stroke:#0e97b0 style BE fill:#e2f5f9,stroke:#0e97b0 style OBO fill:#fbede3,stroke:#b4531b
One resolution point, carried through the request. Because the commit hook reads it from context rather than from an argument, a developer adding a new write cannot forget to populate it — and cannot populate it incorrectly.

The on_behalf_of field is the case that catches people out. An agent frequently acts for a named human — an assistant drafting correspondence for a specific underwriter, an agent executing a task a person queued. That action is genuinely agent-performed and genuinely human-initiated, and if you only have one field you will have to choose which half to lose.

# The full shape. Three fields, all set at the edge. actor_code = "CL-DOKAFOR" # who actor_kind = "agent" # human | agent | system on_behalf_of = "u.okonkwo" # nullable; the person who initiated

What it lets you answer

-- The board question, in one query. SELECT actor_kind, COUNT(*) AS actions, COUNT(DISTINCT correlation_id) AS matters, SUM(amount) FILTER (WHERE amount IS NOT NULL) AS value_moved FROM business_events WHERE occurred_at >= '2027-01-01' AND occurred_at < '2027-04-01' GROUP BY actor_kind;

Reporting value moved alongside the count is worth doing unprompted. Agents typically perform many low-value actions and humans perform few high-value ones, so a count-based figure overstates autonomy dramatically. In our carrier agents account for the large majority of actions and a much smaller share of the money — which is exactly the shape a well-designed authority ladder produces, and a far more reassuring number to present than the raw count.

The follow-up questions

Once the column exists, three harder questions become cheap.

"Which agent actions were never reviewed by anyone?"

Not a criticism — most routine actions should not need review. But you want the number, and you want to know it is concentrated in the categories you intended.

SELECT b.entity_type, b.event_type, COUNT(*) AS unreviewed FROM business_events b LEFT JOIN authority_decisions a ON a.correlation_id = b.correlation_id AND a.outcome = 'escalated' WHERE b.actor_kind = 'agent' AND a.id IS NULL GROUP BY b.entity_type, b.event_type ORDER BY unreviewed DESC;

"Has the agent share changed, and where?"

Trend by month and entity type. A quiet shift of high-value work from human to agent — because a limit was raised, or a new capability shipped — is something you want to notice deliberately rather than discover in an audit.

"When an agent and a human disagreed, who was right?"

Where an escalated action was rejected, or a human later corrected an agent's value, you have a labelled disagreement. Aggregate those and you have a genuine quality measure that no eval can give you, because it comes from the people doing the work.

What this looks like in InsightLense

Because actor kind is a column rather than an inference, the board question is a view rather than a project — and InsightLense reports value alongside count, which is the pair that makes the answer honest.

Actor breakdown Q1 2027 3 kinds
Actor kindActionsShareValue movedValue share
agent8,11681.2%$18.4M22.1%
human1,20412.0%$62.8M75.4%
system6766.8%$2.1M2.5%
Agents perform 81% of the actions and move 22% of the money. A count-only figure would report this deployment as overwhelmingly autonomous. The value column is what shows the authority ladder working — the large movements are still going to people.

The third row is the one worth having. Folding a nightly batch into either of the other kinds produces a number you have to retract, and InsightLense keeps system separate precisely because we had to retract one.

Retrofitting

If you are adding this to a system that has been running a while, the honest advice is: do not backfill by inference.

It is tempting to write a migration that sets actor_kind based on user-id patterns. It will be wrong for exactly the cases that matter — the shared service account, the migration that ran as a person, the agent that was introduced mid-quarter. And once written it is indistinguishable from a recorded fact, so the error is permanent and invisible.

Set it going forward, leave historical rows null, and be able to say precisely when the record became reliable. "We have accurate actor attribution from 1 March" is a defensible statement. A silently inferred history is not.

One column, set at the edge

This is the smallest change on this blog with the largest governance payoff. It is a column, a middleware line, and a commit hook. The reason to do it now rather than when asked is that it cannot be reconstructed later — and the question is coming.

Where do your agents already act on real records?

Tell us that, and what you would need to prove about those actions to an auditor. We will set up a hands-on walkthrough within two weeks.

Request a demo →