"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.
| Kind | What it is | Example | Accountable 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
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 initiatedWhat 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 kind | Actions | Share | Value moved | Value share |
|---|---|---|---|---|
| agent | 8,116 | 81.2% | $18.4M | 22.1% |
| human | 1,204 | 12.0% | $62.8M | 75.4% |
| system | 676 | 6.8% | $2.1M | 2.5% |
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.
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 →