Six weeks after the first agent goes live, someone in finance asks what the model vendor invoice is for. The honest answer in most firms is a shrug. The invoice is one number for the account, the agent is three workflows sharing one API key, and nobody recorded which run spent what.
That matters less because the bill is large — for a back-office agent at a 40-person firm it usually is not — and more because the number is the only way to answer the question that decides the next build: did this workflow pay back? A redeployment agent that costs a few dollars a day and saves one placement a quarter is obvious. A screening pass that re-reads 900 resumes nightly because a status filter is wrong is also obvious, once you can see it.
This walkthrough builds the metering layer: a run ledger, cost captured at the step rather than reconciled from an invoice, cost per outcome, and a cap that stops a loop before it finishes eating the month.
Step one: give every run an id
Nothing works until each unit of work has an identity. A run is one pass of one workflow over one subject: chase timesheets for week ending 2026-09-13; find matches for placement 4711.
create table agent_runs (
run_id uuid primary key,
agent text not null, -- redeployment | timesheet_chase | screener
trigger text not null, -- schedule | webhook | manual
subject_type text, -- placement | timesheet_period | candidate
subject_id text,
started_at timestamptz not null default now(),
ended_at timestamptz,
outcome text, -- completed | no_action | failed | halted_budget
error text
);
Pass the run_id into every step the run makes, including the ATS calls. It becomes the join key for cost, for the access log, and for the decision record a compliance auditor asks about later. One id, three questions answered.
Step two: capture cost at the step, not from the invoice
Vendor invoices arrive monthly, aggregated, and too late to change anything. Record cost where it is incurred: the model response tells you the tokens it used, so write a row when the call returns.
create table agent_run_steps (
step_id bigserial primary key,
run_id uuid not null references agent_runs(run_id),
seq int not null,
kind text not null, -- model | ats_read | ats_write | email | tool
provider text,
model text, -- pinned version string, not "latest"
input_tokens int,
output_tokens int,
cost_usd numeric(12,6),
latency_ms int,
retry_of bigint references agent_run_steps(step_id),
created_at timestamptz not null default now()
);
Four details that save arguments later:
- Record the pinned model string. When spend jumps 40 percent in a week, the first thing you want to know is whether anything switched underneath you. "latest" is not an answer.
- Price in your own code. Keep a small rate table of price per million tokens per model, effective-dated, and compute
cost_usdat write time. Vendor prices change; old rows should keep the price that applied when the call was made. - Count retries separately. A step that fails and is retried twice cost three times.
retry_ofkeeps that visible instead of averaging it away. - Meter non-model steps too, at zero if need be. ATS reads cost no dollars but they cost rate limit, and rate limit is the resource that actually runs out during business hours.
A rate table is boring and worth the ten minutes:
create table model_rates (
model text not null,
effective_from date not null,
input_per_mtok numeric(10,4) not null,
output_per_mtok numeric(10,4) not null,
primary key (model, effective_from)
);
Step three: roll up to cost per run, then per outcome
Cost per token is a vendor's unit. Cost per outcome is yours.
select r.agent,
date_trunc('week', r.started_at) as wk,
count(*) as runs,
round(sum(s.cost_usd), 2) as spend,
round(avg(s.cost_usd), 4) as avg_per_run,
round(max(s.cost_usd), 4) as worst_run
from agent_runs r
join lateral (
select coalesce(sum(cost_usd), 0) as cost_usd
from agent_run_steps
where run_id = r.run_id
) s on true
where r.started_at >= now() - interval '90 days'
group by 1, 2
order by 1, 2;
Then divide by the thing the firm actually sells. For the redeployment agent, spend over the quarter divided by contractors redeployed after an agent-drafted approach. For the timesheet chaser, spend divided by timesheets in before the first deadline that previously came in late. For the screener, spend per candidate scored, next to the recruiter minutes it displaced.
Those denominators live in tables you already built — the outreach log, the timesheet landing table, the screening log — so the join is cheap. What you get is a sentence an owner can act on: this agent costs about eleven dollars a week and has put four finishing contractors back on assignment this quarter. Or: this one costs thirty a week and has produced a list nobody opens.
The second sentence is a result, not a failure. Switching a workflow off with a number attached is a better outcome than keeping it because it was built.
Step four: cap the runaway before it finishes
Every loop is one bad filter away from unbounded. Three limits, checked in the agent, in increasing order of bluntness.
A per-run step budget. Before each model call, count steps in this run. Past the ceiling — twenty for a chase, forty for a match — stop, write outcome = 'halted_budget', and raise an exception for a human. A run that needs sixty steps is not thorough, it is stuck.
A per-agent daily spend cap. One query before the run starts:
select coalesce(sum(s.cost_usd), 0)
from agent_runs r
join agent_run_steps s using (run_id)
where r.agent = $1
and r.started_at >= date_trunc('day', now());
Over the cap, the scheduler skips the run and posts a line to the daily exceptions list the ops lead already reads. Do not silently drop it, and do not let the agent decide its own exception.
A volume guard on the input. If tonight's finishing-soon list has 400 rows and the trailing average is 12, something upstream changed — a status mapping, a date filter, a bulk import. Halt and ask. The expensive incidents are rarely a pricey model; they are a cheap model run 400 times against rows that should have been filtered out two steps earlier.
Caps belong in the agent, not only in the vendor console. A vendor spend limit protects the vendor's billing; a step budget protects the client's ATS from an agent writing 400 notes at two in the morning.
Step five: put it in the monthly note
One small table on the monthly summary, per agent: runs, spend, cost per outcome, halted runs, failed runs. Five numbers. It takes ten minutes to produce once the ledger exists, and it changes the conversation from "how is the AI going" to "the chaser is at four dollars a day and first-deadline misses are down from 31 to 9".
It also makes "not yet" defensible. When a firm asks for an agent that would cost more to run than the hours it saves, the ledger from the last build is the evidence, and the answer is easier to give and easier to hear.
We build this ledger into the first workflow, before the second one exists, because retrofitting it means reconciling an invoice you cannot break apart. If you want that shape of build on your ATS, tell us what you are running — the ATS, contractor headcount, and where the hours are going.