Report
Answer what a past run did and what it cost, and what a month of runs spent, from the ledger alone with gtme runs and one query each
What you'll learn
Your pipelines ran days ago, someone asks what one run did or what this month cost, and all you have is the ledger.
- how to find a past run and print its receipt again
- how to read the ledger's receipt, and how it maps to the one the run printed
- how to check a receipt against the step_events and costs rows it came from
- how to total spend across runs, split into measured and estimated dollars
Goal: what any past run did and cost, read from the ledger alone. You'll also total a week or a month of runs. SPEC.md, the file that fixes what gtme does, states the promise this guide checks in its Report story:
what happened in a run, and what it cost, is always reconstructable after the fact.
Before you start
You need gtme (Install) and a ledger with some runs in it. On your own ledger, go straight to the steps. Otherwise, build the practice history in the next section first, and the outputs will match this page.
This guide needs no API keys and sends nothing past your own disk. demo/enrich charges a pretend $0.01 per record and calls no vendor. The $0.50 in the next section is a number you enter, and nobody is paid.
Build a practice history
Run hello.yaml from See it run twice, add two people to its CSV, and run it a third time:
mkdir -p report && cd report
export GTME_LEDGER=./ledger.db
curl -fsSLO https://raw.githubusercontent.com/gtme-run/gtme/main/examples/hello.yaml
curl -fsSLO https://raw.githubusercontent.com/gtme-run/gtme/main/examples/contacts.csv
gtme run hello.yaml
gtme run hello.yaml
cat >> contacts.csv <<'EOF'
Dana Park,dana@contoso.com,Head of Growth,contoso.com
Eli Brooks,eli@northwind.io,VP Sales,northwind.io
EOF
gtme run hello.yaml
The three runs end with these total lines:
total: $0.0300 (estimated) spent
...
total: $0 spent, $0.0300 avoided via cache (3 records skipped)
...
total: $0.0200 (estimated) spent, $0.0300 avoided via cache (3 records skipped)
The second run found every score still fresh in the ledger and paid for none. 3 records skipped is the three cached scores. Jane's delivery, already made, prints as out: 1 already delivered and isn't counted as a cache skip. The third run paid for the two new people only.
To put a real bill in the ledger too, save a second pipeline as openers.yaml. A copywriter writes an opener by hand for anyone scoring 90 or more, and bills $0.50 each:
prompt: never means gtme waits for an answer instead of asking in the terminal. Run it, record the copywriter's answer and bill with gtme answer (Participants), and run it again:
gtme run openers.yaml
gtme answer openers opener jane.doe@acme.com \
--set opener="Congrats on the Q3 launch." \
--as copywriter --cost 0.50 --measured
gtme run openers.yaml
The first run stops at opener with Jane waiting, since she's the only one top keeps. The second run collects her answer and finishes the same run, so openers counts as one run:
resuming run 01M3QAG9K5PNTN183GS6Y5E65N (openers)
...
opener human/compose 1 1 - 0 - - $0.5000 -
total: $0.5000 spent
--measured marks the $0.50 as a real bill, so the total prints with no (estimated) (the decision record on cost basis, ADR-046).
Steps
-
List the runs:
gtme runsIt prints output similar to the following:
run pipeline status started records in flight 01M3QAG9K5PNTN183GS6Y5E65N openers done 2026-09-29T19:33:12.421Z 5 - 01M3QAG9HSVM8SHXTK9XFB2DHQ hello done 2026-09-29T19:33:12.377Z 5 - 01M3QAG9GX892QZ22J8HVSJDGF hello done 2026-09-29T19:33:12.349Z 3 - 01M3QAG9FFDHY95DHAKSSX0PB1 hello done 2026-09-29T19:33:12.303Z 3 -Runs are newest first, and
startedis in UTC.recordscounts the people each run touched.in flightcounts records still waiting on a person or a vendor, and such a run's status ispending. A dry-run shows asdone (dry), and what it spent is real. A run whose process died shows asinterrupted, and itsin flightcounts the sends that got no response. -
Print one run's receipt from the ledger. This is the third
hellorun:gtme runs RUN_IDReplace
RUN_IDwith theruncolumn of the row you want, here01M3QAG9HSVM8SHXTK9XFB2DHQ, or uselastfor the newest. It prints:run 01M3QAG9HSVM8SHXTK9XFB2DHQ pipeline: hello status: done started: 2026-09-29T19:33:12.377Z finished: 2026-09-29T19:33:12.410Z step adapter in out empty cached filtered failed cost avoided source csv/source 0 5 - 0 - - $0 - score demo/enrich 5 2 - 3 - - $0.0200 $0.0300 keep sql/filter 5 2 - 0 3 - $0 - out csv/deliver 2 1 - 0 - - $0 - out: 1 already delivered total: $0.0200 (estimated) spent, $0.0300 avoided via cache (3 records skipped) records: 5 (out=2 score=3), 3 with a fail verdict (filtered, or a send withheld) config: 3 steps recorded (`gtme freeze 01M3QAG9HSVM8SHXTK9XFB2DHQ` rebuilds the pipeline)This is the table the run printed when it finished, rebuilt from the ledger, with the run-level lines after it. This table shows where to read each answer:
Question Where How many people did it source? The records:line's first number, 5How many did a filter drop? filteredon thekeeprow, 3How many did a step pay for, and how many did it reuse? outandcached:scorepaid for 2 and reused 3How many did it deliver? outon theoutrow, 1, and the line under the table: 1 already deliveredWhat did it cost, and what did the cache save? The costandavoidedcolumns and thetotal:lineHere that's 5 sourced, 3 filtered, 2 new scores paid for, and Dana delivered, with Jane skipped as already delivered. For a resumed run, the table counts each record once, by its latest outcome, across every session, so it's the one to quote. A run recorded by an older
gtmeprints?where its ledger never stored a number, such asavoided. -
To prove a receipt, rebuild its step table from
step_eventsandcosts, the ledger tables it's read from. Each per-record event names its column indetail.outcome:gtme query "WITH latest AS ( SELECT id, step_id, json_extract(detail, '$.outcome') AS outcome, json_extract(detail, '$.avoided_usd') AS avoided, row_number() OVER (PARTITION BY step_id, identity_id ORDER BY id DESC) AS n FROM step_events WHERE run_id = 'RUN_ID' AND json_extract(detail, '$.outcome') IS NOT NULL) SELECT step_id, count(*) AS records, sum(outcome = 'out') AS out, sum(outcome = 'cached') AS cached, sum(outcome = 'filtered') AS filtered, sum(outcome = 'failed') AS failed, sum(outcome = 'already_delivered') AS already_delivered, (SELECT coalesce(sum(amount_usd), 0) FROM costs c WHERE c.run_id = 'RUN_ID' AND c.step_id = l.step_id) AS usd, coalesce(sum(avoided), 0) AS avoided FROM latest l WHERE n = 1 GROUP BY step_id ORDER BY min(id)"Replace
RUN_IDwith the same id, in both places. It prints:{"already_delivered":0,"avoided":0.03,"cached":3,"failed":0,"filtered":0,"out":2,"records":5,"step_id":"score","usd":0.02} {"already_delivered":0,"avoided":0,"cached":0,"failed":0,"filtered":3,"out":2,"records":5,"step_id":"keep","usd":0} {"already_delivered":1,"avoided":0,"cached":0,"failed":0,"filtered":0,"out":1,"records":2,"step_id":"out","usd":0} 3 rowsEvery number matches the receipt, row for row.
recordsis theincolumn. The query keeps each record's latest event at each step, which is how the receipt counts a resumed run. The source row isn't here, because the source's event names no record. The $0.02 onscoreis twocostsrows, one per new person, both withestimatedinbasis. -
List what each run of the last week cost:
gtme query "SELECT r.id AS run, r.pipeline, substr(r.started_at, 1, 16) AS started, coalesce(sum(c.amount_usd) FILTER (WHERE c.basis = 'measured'), 0) AS measured, coalesce(sum(c.amount_usd) FILTER (WHERE c.basis = 'estimated'), 0) AS estimated FROM runs r LEFT JOIN costs c ON c.run_id = r.id WHERE r.started_at >= date('now', '-7 days') GROUP BY r.id ORDER BY r.started_at"It prints:
{"estimated":0.03,"measured":0,"pipeline":"hello","run":"01M3QAG9FFDHY95DHAKSSX0PB1","started":"2026-09-29T19:33"} {"estimated":0,"measured":0,"pipeline":"hello","run":"01M3QAG9GX892QZ22J8HVSJDGF","started":"2026-09-29T19:33"} {"estimated":0.02,"measured":0,"pipeline":"hello","run":"01M3QAG9HSVM8SHXTK9XFB2DHQ","started":"2026-09-29T19:33"} {"estimated":0,"measured":0.5,"pipeline":"openers","run":"01M3QAG9K5PNTN183GS6Y5E65N","started":"2026-09-29T19:33"} 4 rows-7 dayscounts back from now, in UTC. To find one run among many, addAND r.pipeline = 'hello'to theWHEREline. -
Total the month by pipeline and by basis:
gtme query "SELECT r.pipeline, count(DISTINCT r.id) AS runs, coalesce(sum(c.amount_usd) FILTER (WHERE c.basis = 'measured'), 0) AS measured, coalesce(sum(c.amount_usd) FILTER (WHERE c.basis = 'estimated'), 0) AS estimated FROM runs r LEFT JOIN costs c ON c.run_id = r.id WHERE r.started_at >= date('now', 'start of month') GROUP BY r.pipeline"It prints:
{"estimated":0.05,"measured":0,"pipeline":"hello","runs":3} {"estimated":0,"measured":0.5,"pipeline":"openers","runs":1} 2 rowsThe total includes what dry-runs spent and what pending runs have spent so far. The month starts at midnight UTC.
Report the two columns separately. A measured dollar is on a bill already. An estimated one is a count times a rate someone typed, so check it against the vendor's invoice before you quote it.
What you have now
Any run in the ledger, and any week or month of them, answered from the ledger alone. To have Claude Code report this way, paste this line:
Report on my gtme runs: list them, print the receipt of RUN_ID with gtme runs and read sourced from its records line and filtered, cached, and delivered from its table, check its counts and cost against step_events and costs with gtme query, then list this week's cost per run and this month's total by pipeline, with measured and estimated in separate columns.
Replace RUN_ID with the run you're asked about, or last.
Next
gtme runslists every form of the command, includinglast.- The
coststable lists every column a cost row carries. gtme querylists every flag of the command the checks use.