Runs and receipts

A run is an id plus the ledger rows it wrote, and its receipt is the per-step count of records and dollars that those rows back up

What you'll learn

You've run a pipeline, the step table at the end has columns you can't read yet, and you want to know what each one counts and what a run left behind.

  • what each receipt column counts, and what estimated means on the total line
  • what a run writes to the ledger, and what gtme runs reads back
  • why a delivery never happens twice for the same key, target, and scope
  • where --resume picks up a run that stopped

This is hello.yaml from See it run, run armed, and it spends $0 (demo/enrich charges a pretend $0.01 per record) and sends nothing. From the folder that has hello.yaml and contacts.csv, run it against a fresh ledger:

export GTME_LEDGER=$(mktemp -d)/ledger.db
gtme run hello.yaml

The output is similar to the following:

run 01M3FBB6SRKX0B7GMSF2WMBJVW (hello)
...
run 01M3FBB6SRKX0B7GMSF2WMBJVW — done
step    adapter      in  out  empty  cached  filtered  failed  cost     avoided
source  csv/source   0   3    -      0       -         -       $0       -
score   demo/enrich  3   3    -      0       -         -       $0.0300  -
keep    sql/filter   3   1    -      0       2         -       $0       -
out     csv/deliver  1   1    -      0       -         -       $0       -
total: $0.0300 (estimated) spent

The table at the end is the receipt. It and the progress lines go to the terminal's error stream, so a script or an agent reading the command's standard output gets only data (SPEC §8).

What you just saw

Here's the file that produced it:

Each receipt row is one step, named by its id, and each column is a count the ledger can back up. A - in empty, filtered, or failed means zero. cached prints 0, and avoided shows - when the step skipped nothing.

ColumnWhat it counts
inRecords eligible at the step, before any gate or cache. A source has nothing upstream, so its in is 0.
outRecords the step contributed to: it wrote a field, passed a filter, or made a delivery.
emptyRecords that moved on without the step writing any field. Only steps that write fields count it.
cachedRecords skipped because the ledger already had the answer: a fresh field or the same judgment. A delivery already made counts as already delivered.
filteredRecords a filter step stopped.
failedRecords that errored at the step. Each distinct reason prints on its own line under the table.
costWhat the step spent in this run, summed from its cost rows.
avoidedThe adapter's per-record estimate for each record the step skipped. ? means the adapter publishes no estimate.

Every row after the source reconciles: in equals out + empty + cached + filtered + failed, plus records already delivered, gated by when:, or skipped by on_missing:. Read the keep row that way and you get 3 = 1 + 2. In a dry-run, the out row names what it held back, such as 1 held (dry run). The steps and roles page covers which role can fill which column.

The total line carries its basis. demo/enrich multiplies a configured rate, so its dollars are estimated and the total says (estimated). A vendor that reports its own charge records a measured cost, which prints bare, and a run with both splits into $X ($Y measured + $Z estimated). A second run adds what the cache saved, and the ledger page walks through that receipt.

What a run leaves behind

A run is a ULID and the rows written under it. A ULID is an id that sorts by the time it was made. The rows sit in the ledger's runs layer: runs, run_records, step_events, costs, and deliveries (SPEC §3). Here are this run's run_records rows:

gtme query "SELECT i.identity_key, r.state, r.verdicts FROM run_records r
            JOIN identities i ON i.id = r.identity_id"
{"identity_key":"jane.doe@acme.com","state":"out","verdicts":"{\"keep\":\"pass\"}"}
{"identity_key":"bob@globex.io","state":"score","verdicts":"{\"keep\":\"fail\"}"}
{"identity_key":"carol@initech.dev","state":"score","verdicts":"{\"keep\":\"fail\"}"}
3 rows

Jane finished out. Bob and Carol stopped after score, with a fail verdict from keep.

gtme runs lists every run, newest first:

gtme runs
run                         pipeline  status  started                   records  in flight
01M3FBB6SRKX0B7GMSF2WMBJVW  hello     done    2026-09-26T17:13:58.840Z  3        -

gtme runs last rebuilds this receipt from the ledger, across every resume, and gtme show --run last prints its records as JSON, one per line.

Delivery idempotency

A delivery whose key, target, and scope already have a row isn't made again. That's idempotency: doing it again has the same effect as doing it once. Here's the row this run wrote:

gtme query "SELECT target, scope, idempotency, status FROM deliveries"
{"idempotency":"jane.doe@acme.com","scope":"out.csv","status":"accepted","target":"csv/deliver"}
1 rows

The key is the field named by idempotency:, here email. The target is the adapter. The scope is the setting the adapter names: the path for csv/deliver, the campaign for Instantly.

A repeat is skipped with the reason already_delivered (SPEC §8), counted as already delivered. A different path or campaign delivers again, per the decision record, ADR-044. A rule that holds across every target is a suppression group.

accepted means the target took the request. A delivery becomes sent only when the provider attests it, that is, reports back that it sent (ADR-036). A target that updates in place delivers again when the delivered values change, and attio/assert, a registry entry, is the only target that does today (ADR-045).

Resume

--resume continues the same run from each record's last completed step. To see it, point out at a folder that doesn't exist:

  - id: out
    use: csv/deliver
    with:
      path: sent/out.csv
    variables:
      score: demo.score
      note: demo.note
    idempotency: email

Run it in a fresh ledger:

export GTME_LEDGER=$(mktemp -d)/ledger.db
gtme run hello.yaml

The output is similar to the following:

run 01M3FBBGVWXNYMVJEQRT9039NM (hello)
...
run 01M3FBBGVWXNYMVJEQRT9039NM — failed
step    adapter      in  out  empty  cached  filtered  failed  cost     avoided
source  csv/source   0   3    -      0       -         -       $0       -
score   demo/enrich  3   3    -      0       -         -       $0.0300  -
keep    sql/filter   3   1    -      0       2         -       $0       -
out     csv/deliver  1   0    -      0       -         1       $0       -
out: 1 failed — csv/deliver: open sent/out.csv: no such file or directory
total: $0.0300 (estimated) spent
gtme: runner: out: csv/deliver: open sent/out.csv: no such file or directory

Jane's run_records state is keep, the last step she completed. Make the folder and resume:

mkdir sent
gtme run hello.yaml --resume last

The output is similar to the following:

resuming run 01M3FBBGVWXNYMVJEQRT9039NM (hello)
...
run 01M3FBBGVWXNYMVJEQRT9039NM — done
step    adapter      in  out  empty  cached  filtered  failed  cost  avoided
source  csv/source   0   3    -      0       -         -       $0    -
score   demo/enrich  0   0    -      0       -         -       $0    -
keep    sql/filter   0   0    -      0       -         -       $0    -
out     csv/deliver  1   1    -      0       -         -       $0    -
total: $0 spent

It's the same run id, and score wasn't paid for twice. A rerun gets a new id and a second receipt, with the cache covering what was paid. Resume keeps one id and one gtme runs entry.

When a run fails, have your agent run gtme runs last, then gtme run PIPELINE --resume RUN_ID with the id it printed, where PIPELINE is the pipeline file. --resume last takes the newest run of that pipeline, so the id is the safer choice once the pipeline has run again.

That's it. That's a run and its receipt.

Why it's this way

A receipt may not claim more than the run can prove. out used to count every record that advanced, so a step that wrote nothing still reported full output. ADR-053 split that into out and empty and made each row reconcile. The price is a column that's usually zero.

Dollars say whether they were measured or estimated. ADR-046 put a basis on every cost row. Where a vendor's price depends on your plan, you set the rate, so an estimated total is only as good as that rate.

A delivery says accepted until the provider proves more. A vendor can answer "success" for a lead that was stored blank or never mailed, so the ledger records accepted until the provider attests the send.

What it costs. After a resume, the receipt counts only that invocation, so it says $0. gtme runs RUN_ID holds the whole run's cost.

Where it shows up

  • The gate ladder prints this receipt at simulate and dry-run with deliveries held back.
  • Recover is resume as a task, and Report reads receipts and cost rows across runs.
  • gtme runs and gtme show are the lookup pages.