Case: An Approved Agent for Transactional Work

Consider a support representative who pastes a ticket into an assistant: a customer received a cracked item, wants a refund of \$120, and would like a replacement shipped. An agent can read the order, issue the refund, and request the reshipment. The risks of letting it do so are not the ones that make demonstrations nervous. The model rarely invents a customer. The failures that cost money are quieter: the refund call times out after the ledger committed it and a retry pays twice; the process restarts between the refund and the reshipment and starts the ticket over; a lead approves a \$120 refund and the plan is edited to \$450 before it runs; four refunds of \$49 each stay under an approval threshold of \$50; a configuration change quietly removes the threshold; and weeks later nobody can say which refunds the agent made or whether the ledger agrees.

This article builds the system that prevents, detects, and contains those failures, and runs it. It continues in the same fictional company as Case: Enterprise Knowledge Search with Permissions and Freshness. Quillfeather Logistics, its representatives, team leads, orders, ledger, and order system are all invented for this article. No real organization or incident is described, and the measured behavior comes from a local simulation: a rule-based planner stands in for the model, the ledger and order system are small programs, time is simulated, and every number is the output of running that code. The planner is deliberately obedient. It converts ticket text into tool calls without judgment, so the controls are tested against the worst case, a model that does whatever the text says.

The article assumes you have read Agent Runtime Isolation, Tools, and Execution Policies, which introduced the runtime’s policy engine, approvals bound to one call, idempotent execution, run state, and a hash-chained audit, and AI Platform Incident Response and Recovery, whose containment ladder and distinction between reversible and irreversible effects are used in the incident drill. AI Platform Security and Tenant Isolation supplies the habit of testing a control by removing it. The method of defining invariants and checking them against an oracle is the one used in Case: Enterprise Knowledge Search with Permissions and Freshness. This article does not repeat those explanations. It adds what integration requires: one system in which authorization, approval, durable state, reconciliation, and containment depend on one another.

After reading, you should be able to specify which parts of a transactional workflow need authorization, approval, and exactly-once handling and why; explain how an approval stays tied to the exact action; reason about every crash point between a decision and its recorded effect; reconcile agent records with the systems of record and classify the differences; and contain an incident by switching tools off at execution time, selecting the affected work from records, and compensating what cannot be undone.

The code ran with Python 3.12.14 and SQLite 3.53.4 on macOS 26.7.1, using only the standard library. Runs are deterministic.

The work, the rules, and the failures to prevent

Tickets arrive from customer-support representatives. The agent has three tools. lookup_order reads an order. issue_refund posts a refund to the payments ledger. reship_order asks the order system to create a new shipment. Each tool acts for the representative who owns the ticket, within what that representative may do; the agent has no authority of its own beyond that. Representatives belong to a region. Team leads approve what exceeds the representatives’ authority. The rules the company chose are in the table.

RuleValue in this system
Refunds up to the auto limit\$50.00 per order, counting refunds already made on that order. Executed without approval
Refunds above the auto limitUp to \$500.00. Require approval from a lead in the same region whose own limit covers the amount, and who is not the requester
Refunds above the maximumMore than \$500.00 is denied; a person uses the normal refund tool
Refund cannot exceed what was paidCounting every refund already recorded for the order
ReshippingAllowed for shipped orders. Approval is required when the order was paid more than \$300.00
Caps\$1,000.00 per representative per rolling day; \$3,000.00 per hour across the agent
Approval lifetime15 minutes, single use
DelegationA representative’s token ceiling for refunds is \$500.00; a representative with no refund scope cannot refund through the agent

These rules become five invariants, each of which a test or a query can falsify.

InvariantStatementWhere it is checked
AuthorityEvery executed effect traces to a user’s delegation and stays within it, even if the runtime’s policy is wrongRuntime policy plus the downstream system’s own check of the credential
ApprovalA refund above the auto limit executes only with a used approval that names exactly that callApproval binding; a standing query over records
Exactly-once effectRetries, restarts, and repeated proposals produce one effect per logical action, or an explicit unknownIdempotency keys, write-ahead records, lookup
AgreementThe agent’s records and the systems of record agree, or each difference is classified and ownedA reconciliation job that does not trust the agent’s status
ContainmentAny tool can be stopped at the moment of execution, the affected work can be selected, and the effects can be compensated or accepted by a personSwitches read at execution, credential epochs, records

Design decisions and what each gives up

DecisionChosenAlternativeWhat the choice gives up
CredentialsA token per call: subject is the representative, actor is the agent, audience is one system, scope is one operation, ceiling is the representative’s delegation, 60 seconds, bound to a revocation epochOne shared service credential for the agentA credential-minting step on every call, and an epoch to manage. In return, the agent can never exceed the person it acts for
Where limits liveIn the runtime policy, and again as the token ceiling the ledger enforcesOnly in the runtimeTwo places hold a number and can drift. In return, a wrong policy is bounded by the delegation
ApprovalA record bound to a hash of the workflow, tool, and normalized arguments; summary built by the runtime; requester cannot approve; lead of the same region within their limit; single use; 15 minutesApprove “the agent’s run” as a wholeMore approvals when plans change. In return, the approver sees and authorizes what executes
Threshold basisCumulative refunds on the orderPer callMore approvals for legitimate multi-part refunds. In return, splitting does not evade the threshold
Run stateA step table in a database, with a write-ahead mark before each side effectIn-memory loop; or a full workflow engineA small engine to maintain; no timers, versioning, or visibility tooling that an engine provides. See the glossary entry on durable execution
RetriesStable key from workflow, tool, and normalized arguments for the idempotent ledger; at most once plus lookup for the order systemBlind retry everywhereSome steps end in an explicit unknown instead of finishing
AgreementAn independent reconciliation over steps, ledger entries, and shipmentsTrust step statusA job to run and own
StoppingA per-tool switch read at execution time, plus a credential epochA feature flag checked when the ticket arrivesA database read per step

Two of these deserve a comment. The duplicated limit is intentional redundancy and also the main way a configuration can go wrong, which the incident drill exercises. And the runtime is not a workflow engine: it implements the handful of properties the case needs (persisted steps, resumption, keys, waiting for a person) so they can be inspected, and a production system would more likely adopt an engine that provides them and keep the policy, approval, and reconciliation layers above it. The idea of a saga applies as soon as steps must be undone: here, the compensating step for a refund is a ledger reversal that needs a person’s approval.

Implementation

State: workflows, steps, approvals, switches

A workflow row records the ticket, the user, a status, and the policy version in force when it started. A step row holds the tool, the normalized arguments, a status, the idempotency key, the approval it is waiting on, the result, and a baseline observation taken before the side effect. Approvals are separate rows. Switches hold one flag per tool. A decision log records every policy outcome with its rule and the policy version.

CREATE TABLE workflow(id TEXT PRIMARY KEY, ticket TEXT, user TEXT, status TEXT, policy TEXT, created REAL, updated REAL);
CREATE TABLE step(wf TEXT, n INT, tool TEXT, args TEXT, status TEXT, idem_key TEXT, approval TEXT, result TEXT, baseline TEXT,
  attempts INT DEFAULT 0, note TEXT, t_done REAL, PRIMARY KEY(wf, n));
CREATE TABLE approval(id TEXT PRIMARY KEY, wf TEXT, n INT, call_hash TEXT, summary TEXT, requester TEXT, approver TEXT, status TEXT, expires REAL);
CREATE TABLE switch(tool TEXT PRIMARY KEY, enabled INT);
CREATE TABLE decision_log(t REAL, wf TEXT, n INT, tool TEXT, decision TEXT, rule TEXT, policy TEXT);

Step statuses are pending, held (waiting for an approval or a switch), started (intent recorded, effect possibly happened), unknown, done, denied, suppressed (a duplicate proposal), and compensated. A workflow is running, waiting_approval, paused, needs_reconciliation, done, or failed.

Credentials: authority carried by the call

The credential module follows the delegation semantics of RFC 8693: the token names the person whose authority is being used and, separately, the agent acting. It adds two fields the downstream system checks on its own, a ceiling for the amount and the revocation epoch. Raising the epoch invalidates every outstanding token at once, which is the credential half of the kill switch.

class Authority:
    def __init__(self, clock): self.clock, self.epoch = clock, 1
    def mint(self, *, sub, act, aud, scope, max_cents, ttl=60):
        body = json.dumps(dict(sub=sub, act=act, aud=aud, scope=scope, max=max_cents, exp=self.clock() + ttl, epoch=self.epoch), sort_keys=True)
        return base64.b64encode(body.encode()).decode() + "." + hmac.new(SECRET, body.encode(), hashlib.sha256).hexdigest()
    def verify(self, token, *, aud, scope, amount=None):
        raw, sig = token.rsplit(".", 1); body = base64.b64decode(raw)
        if not hmac.compare_digest(sig, hmac.new(SECRET, body, hashlib.sha256).hexdigest()): raise AuthError("bad signature")
        c = json.loads(body)
        if c["exp"] <= self.clock(): raise AuthError("token expired")
        if c["epoch"] != self.epoch: raise AuthError("credential epoch revoked")
        if c["aud"] != aud: raise AuthError(f"token is for {c['aud']}")
        if c["scope"] != scope: raise AuthError("scope")
        if amount is not None and amount > c["max"]: raise AuthError(f"amount {amount} above token ceiling {c['max']}")
        return c

Policy: ordered rules, default deny, named outcomes

The policy function takes the workflow’s user, the tool, and normalized arguments, and returns a decision with the rule that produced it. It reads the agent’s own records to apply the cumulative and cap rules, and it counts steps that are done, started, or unknown as spent, so an in-flight step is not forgotten. The model contributes only the arguments listed in the tool’s schema; anything else, including a text claim of approval, is dropped during normalization.

def decide(self, wf, user, tool, nargs):
    d = DELEGATIONS.get(user); P = self.policy; t = self.clock()
    if d is None: return "DENY", "R0-unknown-user"
    if TOOLS[tool]["scope"] not in d["scopes"]: return "DENY", "R1-scope-not-delegated"
    o = self.orders.orders.get(nargs["order"])
    if o is None or o["region"] != d["region"]: return "DENY", "R2-order-outside-region"
    if tool == "issue_refund":
        amt = nargs["amount"]
        if amt <= 0 or amt > P["refund_max"]: return "DENY", "R3-refund-over-ceiling"
        q = "SELECT COALESCE(SUM(CAST(json_extract(args,'$.amount') AS INT)),0) FROM step JOIN workflow ON workflow.id=step.wf WHERE tool='issue_refund' AND step.status IN ('done','started','unknown')"
        prior = self.db.execute(q + " AND json_extract(args,'$.order')=?", (nargs["order"],)).fetchone()[0]
        if prior + amt > o["paid"]: return "DENY", "R4-exceeds-amount-paid"
        day = self.db.execute(q + " AND workflow.user=? AND workflow.created>?", (user, t - P["window"])).fetchone()[0]
        if day + amt > P["rep_daily"]: return "DENY", "R5-rep-daily-cap"
        hour = self.db.execute(q + " AND workflow.created>?", (t - 3600,)).fetchone()[0]
        if hour + amt > P["agent_hourly"]: return "DENY", "R6-agent-hourly-cap"
        basis = (prior + amt) if self.flags.cumulative else amt
        if basis > P["refund_auto"]: return "REQUIRE_APPROVAL", "R7-refund-needs-approval"
    if tool == "reship_order":
        if o["status"] != "shipped": return "DENY", "R8-order-not-shipped"
        if o["paid"] > P["reship_approval_value"]: return "REQUIRE_APPROVAL", "R9-reship-needs-approval"
    return "ALLOW", "default-allow-after-checks"

Approval: a record bound to one call

The approval summary is built from the normalized arguments and the order record, never from model text. Approving checks the approver’s role, region, and limit, and refuses the requester. Consuming an approval at execution checks status, expiry, and that the hash of the workflow, tool, and arguments is the one that was approved. An amended step therefore cannot ride on an earlier approval.

def approve(self, aid, approver):
    a = self.db.execute("SELECT wf,n,requester,status,expires FROM approval WHERE id=?", (aid,)).fetchone()
    who = DELEGATIONS.get(approver)
    if a is None or a[3] != "pending": return "not-pending"
    if approver == a[2]: return "refused: requester cannot approve"
    if who is None or who["role"] != "lead": return "refused: approver is not a lead"
    if a[4] <= self.clock(): self.db.execute("UPDATE approval SET status='expired' WHERE id=?", (aid,)); return "refused: expired"
    n = self.db.execute("SELECT tool,args FROM step WHERE wf=? AND n=?", (a[0], a[1])).fetchone(); args = json.loads(n[1])
    reg = DELEGATIONS[self.db.execute("SELECT user FROM workflow WHERE id=?", (a[0],)).fetchone()[0]]["region"]
    if who["region"] != reg: return "refused: approver outside region"
    if n[0] == "issue_refund" and args["amount"] > who["approve_limit"]: return "refused: above approver limit"
    self.db.execute("UPDATE approval SET status='approved', approver=? WHERE id=?", (approver, aid)); return "approved"

def _consume(self, aid, wf, tool, nargs):
    a = self.db.execute("SELECT call_hash,status,expires FROM approval WHERE id=? AND wf=?", (aid, wf)).fetchone()
    if not a or a[1] != "approved": return "no approved request"
    if a[2] <= self.clock(): return "approval expired"
    if self.flags.bind_approval and a[0] != self.call_hash(wf, tool, nargs): return "approval was for a different call"
    self.db.execute("UPDATE approval SET status='used' WHERE id=?", (aid,)); return None

Executing a step

The step function is where the pieces meet. It checks the switch at execution time, applies policy, obtains or consumes an approval, writes the intent and a baseline observation before the side effect, dispatches, and records the result. The three crash hooks mark the points at which a restart must be safe: before dispatch, after dispatch and before the record, and after the record.

def exec_step(self, wf, user, n, tool, args, status):
    nargs = args
    row = self.db.execute("SELECT approval,idem_key,baseline,attempts FROM step WHERE wf=? AND n=?", (wf, n)).fetchone(); aid, key, baseline, attempts = row
    if self.flags.check_switch_at_execution and not self.enabled(tool):
        self.db.execute("UPDATE step SET status='held', note='tool disabled' WHERE wf=? AND n=?", (wf, n)); return "paused"
    if status == "unknown": return self.resolve_unknown(wf, n, tool, nargs, baseline)
    resumed = status == "started"
    if resumed and not TOOLS[tool]["idempotent"]: return self.resolve_unknown(wf, n, tool, nargs, baseline)
    decision, rule = ("ALLOW", "resume") if resumed else self.decide(wf, user, tool, nargs)
    self.db.execute("INSERT INTO decision_log VALUES(?,?,?,?,?,?,?)", (self.clock(), wf, n, tool, decision, rule, self.policy["version"]))
    if decision == "DENY":
        self.db.execute("UPDATE step SET status='denied', note=? WHERE wf=? AND n=?", (rule, wf, n)); return "failed"
    approved_claim = (not self.flags.strict_schema) and bool(args.get("approved_by"))
    if decision == "REQUIRE_APPROVAL" and not approved_claim:
        problem = self._consume(aid, wf, tool, nargs) if (status == "held" and aid) else "none"
        if problem:
            if problem in ("approval expired", "approval was for a different call"):
                self.db.execute("UPDATE approval SET status='void' WHERE id=?", (aid,))
            if problem != "no approved request": self.request_approval(wf, user, n, tool, nargs)
            return "waiting_approval"
        self.db.execute("UPDATE step SET note='approved' WHERE wf=? AND n=?", (wf, n))
    # write-ahead: record intent and the pre-state before the side effect
    if self.flags.write_ahead and not resumed:
        base = json.dumps(len(self.orders.shipments[nargs["order"]])) if tool == "reship_order" else None
        self.db.execute("UPDATE step SET status='started', baseline=?, attempts=attempts+1 WHERE wf=? AND n=?", (base, wf, n))
    self._crash("before_dispatch")
    try: res = self.dispatch(wf, user, tool, nargs, key, n)
    except AuthError as e:                                   # the downstream system refused: record it, do not retry
        self.db.execute("UPDATE step SET status='denied', note=? WHERE wf=? AND n=?", (f"downstream refused: {e}", wf, n)); return "failed"
    if res == "unknown":
        self.db.execute("UPDATE step SET status='unknown' WHERE wf=? AND n=?", (wf, n)); return "needs_reconciliation"
    self._crash("after_dispatch")
    self.db.execute("UPDATE step SET status='done', result=?, t_done=? WHERE wf=? AND n=?", (json.dumps(res, sort_keys=True), self.clock(), wf, n))
    self._crash("after_record")
    return "ok"

Dispatch mints a fresh token for each attempt. For tools whose downstream honors a key it retries once with the same key. For the order system, which has no key, it makes one attempt and reports an unknown outcome when the response is lost. An unknown outcome is resolved by asking the order system what exists and comparing with the baseline recorded before the call.

def dispatch(self, wf, user, tool, nargs, key, n):
    d = DELEGATIONS[user]; spec = TOOLS[tool]
    ceiling = d["refund_cap"] if self.flags.downstream_ceiling else 10 ** 9
    tok = self.auth.mint(sub=user, act=self.agent, aud=spec["aud"], scope=spec["scope"], max_cents=ceiling)
    tries = 2 if spec["idempotent"] else 1
    for i in range(tries):
        try:
            if tool == "lookup_order": return self.orders.lookup(tok, nargs["order"])
            if tool == "issue_refund": return self.ledger.refund(tok, nargs["order"], nargs["amount"], key if self.flags.idempotency_keys else f"{key}-{wf}-{n}-{i}")
            if tool == "reship_order": return self.orders.reship(tok, nargs["order"])
        except Timeout:
            tok = self.auth.mint(sub=user, act=self.agent, aud=spec["aud"], scope=spec["scope"], max_cents=ceiling)
            continue
    return "unknown"

def resolve_unknown(self, wf, n, tool, nargs, baseline):
    """An outcome that could not be observed: ask the downstream system what exists, comparing with the pre-state."""
    if tool == "reship_order" and baseline is not None:
        now = len(self.orders.shipments[nargs["order"]])
        if now > json.loads(baseline):
            self.db.execute("UPDATE step SET status='done', result=?, note='resolved by lookup: shipment found', t_done=? WHERE wf=? AND n=?", (json.dumps({"shipment": self.orders.shipments[nargs['order']][-1]}), self.clock(), wf, n)); return "ok"
        self.db.execute("UPDATE step SET status='pending', note='resolved by lookup: nothing found, safe to repeat' WHERE wf=? AND n=?", (wf, n)); return "running"
    return "needs_reconciliation"

Authorization and approval, exercised

The first run applies the policy to representative requests. Each outcome names its rule, which is what makes denials explainable to the person who made the request and countable in monitoring.

1. policy decisions (each names its rule)
  user     order     amount  decision          rule                         note
  rep-ana  O-1001    $30.00  ALLOW             default-allow-after-checks   small refund in region
  rep-ana  O-1002   $120.00  REQUIRE_APPROVAL  R7-refund-needs-approval     above the auto limit
  rep-ana  O-1002   $600.00  DENY              R3-refund-over-ceiling       above the maximum
  rep-ana  O-1003    $20.00  DENY              R2-order-outside-region      order outside the rep's region
  rep-cy   O-1003    $20.00  DENY              R1-scope-not-delegated       scope not delegated
  rep-ana  O-1001   $150.00  DENY              R4-exceeds-amount-paid       more than the order was paid

The order outside the representative’s region is denied by R2 before any credential is minted, and a representative who never delegated the refund scope is denied by R1. The \$150 request on an order paid \$120 is denied by R4 because it exceeds what the customer paid, a check that holds against a model that has been talked into any amount.

The second run tries to approve a \$400 refund wrongly. Each refusal comes from a different rule, and no refund is made.

2. approval authority: workflow W1 asks for $400 on O-1004 (US, rep-ben)
  summary shown to the approver: Refund $400.00 on order O-1004 for customer-4; requested by rep-ben; workflow W1
  approve as rep-ben   -> refused: requester cannot approve
  approve as rep-cy    -> refused: approver is not a lead
  approve as lead-fay  -> refused: above approver limit
  approve as lead-dee  -> refused: approver outside region
  approve as lead-eli after 1000 s -> refused: expired
  refunds executed: 0

The representative who requested the refund cannot approve it. A colleague without lead status cannot. A lead whose limit is \$100 cannot approve \$400. A lead from the other region cannot. The correct lead is too late once 1,000 seconds have passed, because the approval lived 15 minutes. The summary the approver saw was generated from the normalized arguments and the order record, so it describes the call that will execute.

The third run shows what binding buys. A lead approves \$120, the plan is then amended to \$450 (for example because the representative added a second damaged item to the ticket), and the run is resumed.

3. the same approved call versus a changed one
  approve -> approved
  after amending to $450, run -> waiting_approval | refunds: 0 | new approval: ('AP-2', 'pending')

The amended call hashes differently, so the earlier approval cannot be consumed. The engine voids the mismatched approval, creates a new one for \$450, and waits. Without the binding, the test matrix below shows that the \$450 refund executes on the \$120 approval.

Durable execution: exactly once, or an honest unknown

A step performs three things that cannot be made atomic: it decides, it changes another system, and it records that it did. A crash can fall between any two of them. The write-ahead record before the side effect and the stable key turn the ambiguous cases into recoverable ones. The next table crashes a workflow at each hook, restarts the engine as a new process over the same database file, resumes, and counts effects. The right-hand column repeats the run with the write-ahead record removed.

step     crash point         designed: effects    no write-ahead: effects
refund   before_dispatch           1 refund(s)                1 refund(s)
refund   after_dispatch            1 refund(s)                1 refund(s)
refund   after_record              1 refund(s)                1 refund(s)
reship   before_dispatch           1 reship(s)                1 reship(s)
reship   after_dispatch            1 reship(s)                2 reship(s)
reship   after_record              1 reship(s)                1 reship(s)

The refund survives every crash point in both columns, because the ledger honors the key: a resumed step dispatches again with the same key and the ledger replays its recorded result. This is the reason refunds are safe even without a write-ahead record, and the reason the second column matters only for the reshipment. The order system has no key, so a crash after the shipment was created and before it was recorded leaves a step that never learned it ran. With no write-ahead record, the resumed engine sees only a pending step and creates a second shipment. With the record, the step is started on resume, the engine knows a non-idempotent effect may have happened, and it compares the order’s shipment count with the baseline it saved before the call. One more shipment means the effect happened and the step is marked done; no change means it can be repeated safely. The crash before dispatch and the crash after the record both end with one shipment in either column.

The lookup is a resolution, not a proof. It assumes nobody else reshipped the same order between the baseline and the check. If that assumption is doubtful, the step stays unknown and the workflow stops at needs_reconciliation for a person, which is also what happens when the downstream offers no way to look. The glossary entry on the idempotency key describes this indeterminate state; the point of the engine is to make it a recorded state that someone owns, not a silent retry or a silent skip.

Waiting for a person is also state. When a workflow needs an approval it persists as waiting_approval with the approval row, and nothing keeps a process alive for it. The test approval arrives after a restart starts a workflow, discards the engine, opens a new one over the same file, approves, and runs; the refund happens once. That is the property that makes it practical to wait hours for a lead.

Reconciliation: do the records agree with the world

Status fields say what the agent believes. reconciliation compares the agent’s records with an independent reference, here the ledger and the order system, and treats each difference as something to classify, not to overwrite. The job reads steps, ledger entries, and shipments, joins refunds by idempotency key, and reports eight classes. The run below seeds one example of each failure, then restarts the engine.

matched                                 4  
agent_done_ledger_missing               1  [('WP', 1, 'O-1003', 1000)]
amount_mismatch                         1  [('W0', 2, 'O-1001', 2000, 200)]
ledger_entry_without_step               1  [('L7', 'O-1008', 5000, 'finance-clerk')]
in_flight_ledger_has_it                 1  [('WC', 1, 'O-1007')]
in_flight_ledger_lacks_it               0  []
refund_over_limit_without_approval      0  []
shipment_count_mismatch                 1  [('O-1004', 1, 0)]

Four refunds match. The five seeded differences are all visible. A refund the agent recorded as done is missing from the ledger, which a storage fault that acknowledged and then lost the write produces. A ledger amount of \$2.00 against an agent amount of \$20.00 comes from an adapter bug. A ledger entry by a finance clerk has no step; it is external, and the job reports it without claiming it is wrong. One workflow is in flight with the ledger already holding its entry, which is a crash between effect and record that has not been resumed yet. And an order has a shipment that no step created.

repair class for each non-empty finding
  agent_done_ledger_missing: re-check the ledger; if truly absent, replay with the same key (safe: idempotent) after a human confirms
  amount_mismatch: freeze both records, finance decides which is right; never overwrite silently
  ledger_entry_without_step: classify: external or manual entry (no action) versus lost agent record (adopt by key)
  in_flight_ledger_has_it: resume the workflow; the ledger replays the recorded result

The repair column is a decision about who may act. Resuming an in-flight workflow is safe, because the key makes it idempotent, and restarting the engine resolves it: after the restart there are five matched refunds and no in-flight steps. Adopting a ledger entry as a lost agent record, replaying a refund the ledger lost, and canceling an extra shipment all change money or a customer’s order, so they go to a person. Where two records disagree on an amount, the job freezes both and does not decide. Reconciliation has the limit that its reference can share the agent’s error. If the ledger and the agent both record the wrong amount, they agree. Comparing with something independent, such as the payment processor’s statement, is a further reconciliation layer.

Tests, and what they establish

As in Case: Enterprise Knowledge Search with Permissions and Freshness, each test must pass on the designed system and fail when its control is removed. A test that passes either way is not testing the control.

test                                          designed   control removed  removed control
lost response, retried                            pass              FAIL  idempotency_keys=False
model repeats the same call                       pass              FAIL  run_dedupe=False, idempotency_keys=False
crash at each point, then resume                  pass              FAIL  write_ahead=False
approval arrives after a restart                  pass               n/a  -
plan amended after approval                       pass              FAIL  bind_approval=False
refunds split to stay under the threshold         pass              FAIL  cumulative=False
approval claimed in tool arguments                pass              FAIL  strict_schema=False
policy bug, token ceiling holds                   pass              FAIL  downstream_ceiling=False
tool switched off mid-run                         pass              FAIL  check_switch_at_execution=False
approver authority rules                          pass               n/a  -

layering: the repeated-call test with one layer removed at a time
  run_dedupe=False only: pass | idempotency_keys=False only: pass

Eight tests were run with a control removed, and each failed as it should. With idempotency keys removed, a lost response is followed by a second refund. With the write-ahead record removed, a crash after the shipment duplicates it. With approval binding removed, the amended \$450 refund runs on the \$120 approval. With the cumulative basis removed, four refunds of \$49 on one order all execute. With strict argument normalization removed, a model-written approved_by field is accepted as an approval. With the downstream ceiling removed, a \$700 refund passes a mistaken policy. With the switch check at execution removed, a refund runs after its tool was switched off, because the workflow had already passed the check at admission. The repeated-call test needed both the run-level duplicate check and the ledger’s key removed before it failed, and the last two lines show why: either layer alone catches the duplicate. Layered defenses cannot be mutation-tested one at a time without hiding behind each other, so the test removes both.

The two tests marked n/a have no switch. The restart test and the approver-authority test pass on the designed system and have not been shown to fail on a broken one. What the suite establishes is that these named sequences are handled and that these controls are load-bearing. It does not establish that no sequence leads to a duplicate or an unauthorized refund. It runs one engine against a ledger that never loses idempotency state, and a real concurrent deployment would add races between two engines picking up the same workflow, which needs a lease or a single-writer rule that the sketch does not implement.

An incident: a policy release removes the approval step

The last drill combines everything. Over three simulated hours, tickets arrive on average every 90 seconds. About 70% request a refund under \$50, 22% between \$51 and \$500, and 8% above \$500. About a third also ask for a reshipment. The drill raises the caps to \$5,000 per representative per day and \$10,000 per hour so that they do not dominate the traffic. Leads check their approval queue every two minutes and approve 60% of what is pending. One hour in, a new policy bundle, p2, is released. Its author expressed the auto limit and the maximum in dollars where the engine reads cents, so the auto limit is \$5,000 and the maximum \$50,000. The policy change gate from Prompt and Model Promotion with Evaluation Gates would have rejected it; the gate here is a table of boundary cases, and the next output shows candidate p2 failing three of them.

4. a policy change gate: the same cases run against a candidate bundle
  p1 (current)                     cases failing: 0 []
  candidate with a units mistake   cases failing: 3 [(51, 'REQUIRE_APPROVAL', 'ALLOW'), (500, 'REQUIRE_APPROVAL', 'ALLOW'), (501, 'DENY', 'ALLOW')]

The bundle was released without running that gate. The drill follows what happens. The delays between steps (ten minutes from alert to containment, fifteen to a corrected policy) are assumptions of the simulation, not measurements.

timeline (minutes after the policy release):
  deploy    +  0.0
  alert     +  5.0
  contain   + 15.0
  fix       + 30.0
  reenable  + 35.0
workflows paused at the time of the fix: 11 | after re-enabling they ended as: {'failed': 1, 'done': 10}
policy   refund steps  executed   above $50, no approval   blocked by token ceiling  denied by policy
p1                 29        27        0 (    $0.00)                          0                 2
p2                 20        17        3 (  $827.00)                          1                 2
p3                 69        59        0 (    $0.00)                          0                 7

reconciliation worklist: 3 refunds executed above the auto limit without an approval, total $827.00
  by rep: {'rep-ana': 2, 'rep-ben': 1} | first at +1.8 min | last at +13.8 min
remediation: ratified 1; reversed 2 in the ledger, $769.00 to recover from customers (outside the system)
reconciliation after remediation: {'agent_done_ledger_missing': 0, 'amount_mismatch': 0, 'ledger_entry_without_step': 0, 'in_flight_ledger_has_it': 0, 'in_flight_ledger_lacks_it': 0, 'refund_over_limit_without_approval': 0, 'shipment_count_mismatch': 0} | matched: 101
since the fix: 25 refunds executed under the auto limit, 34 executed with a used approval; violations: 0
  n=34: 95% upper bound on the violation rate with zero violations = 0.0843
  n=300: 95% upper bound on the violation rate with zero violations = 0.0099
  n=1000: 95% upper bound on the violation rate with zero violations = 0.0030

Detection came from an invariant, not from a symptom. A monitor ran every five minutes and asked the records one question: has any refund above the auto limit executed without a used approval? The first violation happened 1.8 minutes after the release and the monitor saw it at 5.0. Containment at 15 minutes did two things at once. It switched the refund tool off in the switch table, which the engine reads at each step, so workflows already in progress stopped at their next refund instead of finishing it. And it raised the credential epoch, which invalidated tokens already minted. The last violation was at 13.8 minutes, so nothing leaked after containment. Eleven workflows were paused at the time of the fix. After the corrected bundle p3 was released and the tool was re-enabled, those eleven resumed under the corrected policy: ten finished, with approvals where required, and one failed because its \$684.00 refund exceeds the corrected \$500.00 maximum.

The blast radius had two bounds. The p2 workflows executed 17 refunds, of which 3, totaling \$827.00, went above the auto limit without approval; the misconfigured maximum let a request above \$500 reach the ledger, and the ledger refused it against the representative’s \$500 ceiling. That refused request is the second copy of the limit doing its job. The damage that did occur was bounded by the delegation and by the number of tickets in the 14-minute window, not by the unit error.

Scoping used the records, not the clock. The reconciliation worklist selects refunds by their own properties (amount, approval status, rep) and not by the time window of the bad bundle, because a time window misses effects of paused workflows and includes unaffected ones. It listed three refunds, two by one representative and one by the other. Compensation is the part that cannot be automated, and it follows the split in AI Platform Incident Response and Recovery between effects the platform can reverse and effects that left it. A lead reviewed each item. One was ratified, with a retroactive approval recorded as such because the ticket was a damaged-goods case with a replacement shipment. Two were reversed in the ledger, which creates a reversal entry but does not return money: \$769.00 remains to be recovered from the customers by a process outside the system. After remediation, reconciliation shows zero unexplained differences and 101 matched refunds. That statement is about agreement between records, and it does not say the company recovered the money.

Re-enabling was staged by evidence. After the fix there were 25 auto-approved refunds and 34 refunds with a used approval, and no violations. Zero violations in 34 approval-gated refunds bounds the violation rate at 8.4% with 95% confidence by the exact one-sided formula \( 1 – 0.05^{1/n} \), which is close to \( 3/n \). That number is too weak to relax anything, which is the useful reading: the evidence supports the corrected policy as designed, and it would take about 300 clean requests to reach 1% and about 1,000 to reach 0.3%. The structural controls, not the sample, carry the guarantee: the ceiling, the approval binding, and the invariant monitor.

The postmortem questions follow the same pattern as the earlier incident article. The monitor fired because the invariant was written at the effect, not at the configuration. The limit existed in two places and the second one limited the damage. The policy bundle shipped without its boundary cases, so the case table becomes a required check on every bundle. The approval basis was correct and was bypassed by configuration, so a configuration change that raises a limit by orders of magnitude should itself require approval. And an alert for “approvals per hour fell to zero” would have detected the failure even before a violating refund was executed, which suggests a second monitor on process shape in addition to the one on outcomes.

What remains unsolved

  • Concurrency. The engine assumes one process works a workflow at a time. Two workers picking up the same workflow need a lease or a single-writer partition, and the failure modes of a stale lease are their own design.
  • Downstream semantics. The ledger here honors keys perfectly, rejects mismatched parameters, and remembers every key. Real services expire keys, return errors that hide committed effects, and have rate limits. Each such property changes which steps are safe to retry.
  • Lookup is not proof. Resolving an unknown reshipment by counting shipments is correct only when nothing else creates shipments for that order concurrently.
  • The planner is a stub. A real model produces ambiguous plans, hallucinated order numbers, and adversarial text. The controls here do not depend on its behavior, but the rate at which it proposes useful, correct actions is a separate measurement and a separate cost.
  • Approval quality. A lead who sees many requests may approve without reading. The threshold, the summary, and the queue design determine whether approval is a control or a ritual, and the simulation’s 60% approval rate says nothing about human attention.
  • Compensation is a business process. A ledger reversal records an intention. The money, the customer relationship, and any legal duty sit outside the system.

The system keeps authority at the person’s level, makes approvals specific to the action, treats every step as something that may need to be resumed or explained, compares its own records with the world, and can be stopped at the moment of effect. The previous case read documents on behalf of employees and the one before it governed what a coding agent may run. The next case, Case: Deploying AI Development Tools Across Engineering Teams, turns to the organization around those tools: how a company introduces them to many teams, measures whether they help, and decides how far to go.

References


Discover more from Insightful Data Lab

Subscribe to get the latest posts sent to your email.

Similar Posts

Questions, corrections, or additional insights?

This site uses Akismet to reduce spam. Learn how your comment data is processed.