Chapter 5 of 7 · 15 min read
Verifying a completed run
On this page
A nightly agent that logs into a supplier portal, downloads a report and posts it into a finance system has two ways to fail. It can stop — a crash, a timeout, a killed process — and the machinery teams build for that is well understood: checkpoints record where it got to, idempotent actions make a re-run safe, an audit trail supports the reconstruction, and a kill switch plus a named owner decides what to do. That machinery is genuinely necessary, and the final chapter of this topic is about building it properly, because interruptions are common and expensive. Or it can finish. Every step succeeds, the process exits zero, the audit trail is complete and accurate, and the thing it did was wrong.
This chapter is about the second failure, which is the one that survives longest, because every signal a system emits about it is a success signal. Verification here means comparing what a run produced against an expectation formed independently of the run, before it started. That is a different class of control from anything in the recovery path, it lives in different code, and it is the only thing standing between a confidently wrong agent and however many nights pass before a human notices.
Liveness controls are silent about correctness
Take the specific failure this chapter is built on: a supplier portal ships a redesign, the report the agent has been downloading for four months moves two clicks away, and the agent downloads a different report with a plausible name and uploads its rows into accounts payable.
A checkpoint records that step 14 completed, which it did. Idempotency — the property that re-running a step leaves the same end state rather than producing a second one — guarantees that re-running produces the same outcome, which here is the problem rather than the fix, because the wrong report is ingested consistently rather than intermittently. An audit trail is evidence, not a detector, and evidence nobody reads at 02:10 is a file. Retries are never triggered because nothing fails. A kill switch is never used because nobody knows there is anything to kill. Even a rollback owner is stuck, because rollback answers "undo the run that failed" and this run succeeded by every definition the system had. The gap is that the system had no definition of right, only definitions of finished.
Every one of those controls fails the same test, and the test is worth more than the list. A defence has to read differently in the case it is meant to catch than in the case it is not, or it is not discriminating anything. So put the two nights side by side. Exit status: zero on both. Audit trail: complete on both. Checkpoint: the same steps recorded as done on both. Retry logic: never invoked on either. Kill switch: never pulled on either. Not one of those readings tells the night the right report was ingested apart from the night the wrong one was, which means not one of them carries any information about which night it is. Idempotency is worse than uninformative here, and it is the single place in this topic where a genuinely good property points the wrong way. Re-running produces the same end state, so the guarantee that makes recovery safe is also the guarantee that a wrong report will be ingested identically every night until somebody outside the system happens to look.
The controls that catch a confidently wrong completion share one property: they compare what happened against an independent expectation, formed before the run, from something other than the agent's own account of itself.
Four gates in that diagram do different jobs and none of them substitutes for another.
Preconditions assert the shape of the thing before any write happens. In this
failure the cheapest of them would have been decisive: the downloaded file's
header row for the invoice register is
invoice_no,invoice_date,po_number,net,vat,gross,currency, and the statement of
account's is date,description,debit,credit,balance. A single set comparison,
costing one line, fails the run on the first bad night. Shape assertions extend
past the header — the period the report claims to cover, the number of rows
falling in a plausible band, the presence of a known control record, whether
every date sits inside the requested window.
Reconciliation compares what landed against an independently obtained figure. The portal's own summary page shows a total for the period; the downloaded file has a sum; the ERP staging table has a sum after transformation. Three numbers derived by different routes that must agree to the penny is a much stronger statement than any single one of them being present.
The strength comes from the routes rather than from the count, and it is worth being exact about that. The portal's summary is computed by the supplier's software from the supplier's records. The file's sum is computed by your code from bytes the portal handed over. The staging table's sum is computed after your transformation has renamed columns, parsed dates and coerced currencies. For all three to agree to the penny, the download has to be the right report, the transfer has to have been complete, and the transformation has to have preserved value — three separate ways the night can go wrong, each of which breaks the agreement on its own. One number being present proves that a field existed. Three computed by different paths agreeing is a statement about the whole path.
That is also why one cell of the comparison below is the finding rather than a hole in the record. On the night the wrong report arrived there was no comparable field to reconcile against at all, because a statement of account does not carry an invoice gross total. A reconciliation you cannot compute has not passed; it has failed to be defined, and those two outcomes look identical to any code that treats a missing comparison as a skipped one. The check has to be written so that the absence of the field is itself a quarantine, which is a line of code and an intention, and the intention is the part that goes missing.
Bounded blast radius per run limits the damage a wrong-but-plausible run can do before anyone looks. A cap of 500 rows and £250,000 of value per run, plus a rule that a run may not differ from the trailing thirty-run median by more than 40%, converts an unbounded nightly write into a bounded one. Runs outside the bounds do not fail; they stage and wait, which is a different and much more useful outcome than either committing or crashing.
Distribution alerts catch the cases nobody wrote an assertion for. The agent's own behaviour has a stable signature — this job took 34 steps ± 3, 3m 40s ± 25s, and roughly 380,000 input tokens per run. The run against the redesigned portal took 21 steps and 2m 12s, because the wrong report was two clicks closer than the right one, and that was visible in the metrics from the first bad night onward while nothing else was.
The assertions, in the place they have to live
The essential detail is that these checks run against the artefacts, not against the model's report of what it did. An agent asked "did you download the invoice register?" will say yes, because it believes it did, and the check would then be circular.
from decimal import Decimal
import pandas as pd
EXPECTED = {"invoice_no", "invoice_date", "po_number", "net", "vat", "gross", "currency"}
ROW_BAND = (60, 500)
MAX_VALUE = Decimal("250000")
DELTA_LIMIT = Decimal("0.40")
class Quarantine(Exception):
"""Halt before any write; the run is preserved for a human."""
def check_source(path: str, period_start, period_end, portal_total: Decimal):
df = pd.read_csv(path)
if set(df.columns) != EXPECTED:
raise Quarantine(
f"header mismatch: missing={EXPECTED - set(df.columns)} "
f"unexpected={set(df.columns) - EXPECTED}"
)
if not ROW_BAND[0] <= len(df) <= ROW_BAND[1]:
raise Quarantine(f"row count {len(df)} outside {ROW_BAND}")
dates = pd.to_datetime(df["invoice_date"])
if dates.min() < period_start or dates.max() > period_end:
raise Quarantine(f"dates {dates.min().date()}..{dates.max().date()} outside window")
total = Decimal(str(df["gross"].sum())).quantize(Decimal("0.01"))
if total != portal_total:
raise Quarantine(f"file total {total} != portal summary {portal_total}")
return df, total
def check_bounds(total: Decimal, trailing_median: Decimal):
if total > MAX_VALUE:
return "stage_only", f"value {total} over per-run cap {MAX_VALUE}"
if trailing_median and abs(total - trailing_median) / trailing_median > DELTA_LIMIT:
return "stage_only", f"value {total} deviates >40% from median {trailing_median}"
return "commit", ""That is forty lines. The post-write read-back belongs alongside it: after committing, query the ERP for the batch just written, sum it, and compare against the source total, because the transformation and the upload are two more places a number can change. Anything that fails goes to quarantine with the artefacts attached — the CSV, the screenshots, the assertion that failed, and the run identifier — so that the human who picks it up in the morning has the evidence rather than a page reference.
A worked comparison of two nights makes the difference concrete:
| Night 120 (correct) | Night 122 (wrong report) | |
|---|---|---|
| Exit status | 0 | 0 |
| Steps taken | 33 | 21 |
| Duration | 3m 44s | 2m 12s |
| File header | invoice_no, invoice_date, … | date, description, debit, … |
| Rows written | 388 | 412 |
| Gross total vs portal summary | equal | no comparable field |
| Caught by, with the gates in place | — | header assertion, step 1 of the pipeline |
| Caught by, as built | — | a controller, 11 days later |
The signals that were already being collected
Nothing new had to be measured to catch this. The step count, the duration and the token usage of every run were already in the observability stack, because the platform team had instrumented the agent loop for cost reporting three months earlier. They were on a dashboard nobody had a reason to open, with no alert attached, because cost was the purpose and cost had not changed enough to matter — the wrong report was cheaper, and a 42% drop in spend on an eleven-pound-a-month job is invisible.
The unit is the entire failure in that sentence. Forty-two per cent is an enormous relative change, and the alert was denominated in currency, where the same change comes to a few pounds a month — below the noise on any threshold anybody would be willing to set, and below the rounding on the invoice it appears in. The signal was present, collected, stored and drawn on a chart every night for nine nights, and it was unreadable in the units it had been collected in. Re-express the same event in steps and it is thirteen steps missing out of thirty-four, which no reasonable threshold survives. Nothing about the instrumentation needed to improve. What needed to change was which of the already-collected numbers the threshold was attached to, and that is a choice about units rather than about coverage.
Turning those into a detector is a threshold rather than a project. A run whose step count falls outside the trailing thirty-run interquartile range by more than a factor of 1.5, or whose duration halves, is worth a message in the ops channel even when it exits zero, and on this system that rule produced one alert in the six months before the incident and would have produced one on night 122. The reason to state the rule against the trailing distribution rather than against a fixed number is that agents drift for legitimate reasons — a model upgrade changed the median step count from 34 to 29 in a single deploy — and a fixed threshold either fires constantly after such a change or is quietly raised until it never fires.
Work out which half of that rule fires, because a reader who treats the disjunction as decoration will build the single-signal version and get a different answer. This job's normal shape is thirty-four steps give or take three. The night the wrong report was ingested it took twenty-one, which is not near the edge of that spread but a long way outside it, so the step-count arm fires with room to spare. Now take the duration arm on its own. The comparison night ran in three minutes forty-four and the bad night ran in two minutes twelve, which is a large drop and is not a halving, so that arm does not fire. The alert exists because the rule is a disjunction, and the arm that carries it is the one counting steps. That is structural rather than lucky. The wrong report was two clicks closer than the right one, so the shortfall appears first in a count of clicks; anything measured in seconds also absorbs page load times, retries and network variance, which is exactly the noise that makes a halving threshold necessary and then makes it insensitive.
A semantic canary is the cheapest of the detectors and the one teams find easiest to accept, because it needs no statistics. Pick a record that must always be present in the output: a specific supplier's monthly retainer invoice that appears in every period, or a control row the portal itself includes. Assert its presence and its exact value. It catches the wrong report, the wrong date window, a silently truncated download, and a transformation that drops rows, and it does so with one comparison whose failure message is legible to a non-engineer at 07:00.
One test catching four faults sounds like luck and is not, so it is worth seeing why. A record that must appear in every period, at a known value, is absent if the wrong report was downloaded, absent if the date window slipped, absent if the download truncated, and absent if the transformation dropped rows. Four different faults, one assertion, because all four break the same underlying thing: the correspondence between the file in front of you and the period it claims to describe. What the canary cannot see is any fault that leaves that correspondence intact and corrupts something inside it — a currency column parsed with the wrong decimal separator, say, which keeps every row and every date and changes every number. That is what the totals are for, which is why the canary sits alongside the reconciliation and not instead of it.
The quarantine queue is the part that decides whether this works
A run that halts before writing has converted a silent error into an operational task, and the task needs a name against it before the first quarantine rather than after. In this organisation the queue went to the ops team with an eight-hour response target, a runbook with four options — re-run, ingest manually from the portal, skip the period, escalate to the vendor — and a rule that a quarantined run does not clear itself on the next night's schedule. Without that last rule the nightly job would have run again at 02:10 and either succeeded into the same wrong state or produced a second quarantine, and a queue that grows while nobody looks is the failure mode this whole design was built to avoid.
Bands that fire weekly get ignored within a month regardless of what the runbook says, so the tuning target is the triage rate rather than the coverage. A supplier's genuine invoice spike in December and a period with 38 rows because of a bank holiday are both legitimate, both outside a tight band, and both cheap to handle when the consequence is staging rather than failure.
What verification is not, and what it cannot see
This is not model evaluation. Nothing here measures whether the agent is good at reading portals, and nothing improves with a better prompt. The checks treat the agent as an untrusted producer of artefacts and evaluate the artefacts, which is why they keep working across a model upgrade, a portal redesign, or a swap to a deterministic scraper.
It is also not a replacement for the recovery machinery. Verification tells you that a recovery is needed; it does not perform one, and a quarantine with no owner is an outage with extra steps. On the night the header assertion fires, somebody has to decide whether to re-run against the new page layout, ingest manually, or skip a day.
The honest limitation is that assertions encode an expectation, so they catch deviation from what you expected and are blind to a wrong expectation. If the invoice register had always been the wrong report — if the agent had been pointed at the statement of account since night one — every check above would pass forever, because the baseline would have been built from wrong data. That failure needs a different control, which is a human comparing the output to reality once at the start and then periodically, and the periodic version is the one that gets dropped from the plan.
A run that completes is not a run that was right, and no amount of liveness machinery closes that gap, because every signal it produces is a success signal. What closes it is a small number of comparisons made against the artefact rather than against the agent's account of itself: the header set, the date window, a total that must agree with a figure obtained by another route, a cap on rows and value, and an alert when a run's own shape leaves its own distribution. Forty lines of that would have failed on night 122 instead of eleven days later.
The gates bound how wrong a single run can be. They do not touch the question of why that agent had write access to accounts payable at all, or who decided it should — and in this organisation nobody had, because the ERP service account it used was one the integration team already had lying around, with permissions across far more than payables.
Computer use, browser use and code execution tools — read the current version at https://daviddominguez.dev/learn/en/computer-use-browser-use-and-code-execution-tools/verifying-a-completed-run