Skip to content
Agent Engineering Lab
Pattern

Decision Record

Every enforced action leaves a replayable record carrying its policy version.

Kind
Pattern
Layer
Evidence
Stage
Build
Status
Restated. Nearest prior description:
  • End-to-end action record, an audit log entry with identity, scope, resource and correlation id, Microsoft
Forces
  • A log line records an outcome; the policy that produced it does not stay fixed long enough for that outcome to still mean anything once the version has moved on
  • What a record needs to prove a decision was reasoned is not what a dashboard needs to prove a system was healthy, and one store built to answer both ends up answering neither well
  • The fields cheapest to skip, a detector that cleared, an override with no stated reason, are the two a review needs first, and their absence is not silence, it is an unanswered question
Decision Record pattern diagram

The problem

An enforcement action produces a fact in the instant it happens: this request was blocked, that response was redacted, this transfer waited on a human. Months later, someone in risk, in legal, or in a regulator’s office asks why. What they are usually handed is a log line: blocked, a timestamp, maybe a category name. That line answers none of the questions an audit needs. Which rule fired. At what threshold. Against which policy version. What the system would have done instead, had the confidence band landed one row over. Whether a human later overrode it, and on what basis. None of that survives inside a word describing the outcome.

The gap is not a logging oversight. It is a category error. An outcome is a fact about the world the request touched. A decision is a fact about the reasoning behind it, and reasoning is only auditable if it is written down at the moment it happens, because the detector versions, the policy version, and the band a verdict landed in do not persist anywhere else. ARCH-004 specifies the record built to close that gap. This pattern is about why the record has to exist: without one, an enforcement action is worth nothing as evidence, however correct it was at the time.

Forces

A log line records an outcome; a decision record has to survive the policy’s own churn. Policy changes on a cadence code rarely does: a threshold tightens after an incident, a rule is added for a new category, a default loosens because it blocked too much legitimate traffic. A decision reviewed after later revisions is unintelligible unless the version that produced it travels inside the record, rather than living only in a deploy history nobody checks against an old timestamp.

What proves a decision was reasoned is not what proves a system was healthy. Latency, error rate, uptime: real signals, owned by a different audience, needed at a different retention. A single store built to answer “was this decision correct” and “is the service up” at once serves both badly, because the two questions want different fields, access, and lifespans.

The fields cheapest to omit are the two a review needs first. Recording only the detector that fired says nothing about the detectors that cleared, so “was this even checked” has no answer for the requests that passed. Recording only that a human overrode a block, with no stated reason, turns a control’s most informative signal, that a rule overridden almost every time it fires is not a rule, into an unexplained number. Both omissions stay invisible until a review needs exactly the field never written.

The pattern

State the rule plainly: every enforced action writes one record, at the moment of decision, and that record only counts as evidence if it lets someone reconstruct, later, exactly what happened without needing the surrounding log stream or tribal memory of what a policy used to say.

That requirement has a specific consequence for what the record must hold. An action taken today under one policy version means nothing once that version has been edited twice more, unless the version itself rides inside the record instead of being inferred from when the decision happened. ARCH-004 specifies the shape:

interface DecisionRecord {
  request_id: string;        // correlates across every chokepoint for this request
  chokepoint: 'upload' | 'retrieval' | 'prompt' | 'response' | 'tool' | 'logging';
  principal: string;         // who, or which agent, on whose authority
  detectors_run: { id: string; version: string; cleared: boolean }[];
  verdicts: { detector: string; verdict: string; band: 'near' | 'above'; categories: string[]; evidence_ref: string; evidence_hash: string }[];
  policy_version: string;    // which policy produced `action`, not whichever is live now
  action: 'allow' | 'block' | 'redact' | 'mask' | 'escalate_for_review' | 'require_approval';
  decision_source: 'policy' | 'detector' | 'human' | 'timeout' | 'override' | 'fallback'; // what actually produced `action`
  degraded: boolean;         // true if a detector ran reduced, or timed out
  latency_ms: number;
  outcome: 'completed' | 'blocked' | 'pending_approval' | 'overridden';
  override?: { who: string; when: string; reason: string };
}

Two fields carry weight the rest of the record doesn’t. policy_version is what makes the record replayable rather than merely descriptive: load the policy live at that version, feed it the recorded verdicts, and the action taken should reproduce, the actual test of whether a record counts as evidence. override, when present, is what turns a human’s dismissal of a control into data rather than an anecdote: a stated reason, a name, and a timestamp are the only way a later review can tell a legitimate exception from a control nobody trusts.

Two more fields matter for the same reason. Each verdict carries evidence_ref, a pointer into the content archive Split the Log specifies, and evidence_hash, an integrity check confirming that span has not changed since. Neither carries the matched span itself: a record is evidence about a decision, not a copy of what the decision concerned, and inlining the actual leaked fragment turns an audit trail meant to prove a control worked into a second, less-protected copy of the data it exists to protect. decision_source names what actually produced action: policy or detector for an automated call, human for a person’s judgment, timeout for an expired clock, override for a person overruling the policy, fallback for a degraded-mode default. Approval Gate is why this field exists: an allow reads identically whether a human approved it or a clock expired under on_timeout: proceed, and without it an audit cannot tell a decision from an accident.

Worth being precise about what this pattern is not. It is not operational telemetry, latency and error rate belong to a different store, a different audience, a different retention, and it is not a content archive, the flagged document or the redacted account number is a different kind of asset, with access rules stricter than the record’s own. Split the Log argues that three-way separation in full; a decision record is one of the three stores it names, the one whose job is provenance of the enforcement action, not the system’s health and not the content that triggered it.

The nearest prior description is Microsoft’s end-to-end action record: identity, scope, resource, and a correlation id, tying an action back to a specific request. This pattern restates that shape for the enforcement case, not a new mechanism. What it adds is narrower: the record is only as useful as its ability to be replayed against the policy that produced it, so the version identifier is the field the rest of the record exists to support.

Worked example

Take an agent at a regulated institution allowed to submit wire instructions, its tool-execution chokepoint in front of anything that moves money. A wire request above a set amount lands in the near-threshold band on a sanctions-screening detector. Policy version 2026-03-11.2 maps that combination to require_approval. A human approver reviews it within the hour, confirms the counterparty is legitimate, and overrides the hold: a false positive against an updated sanctions list. The transfer completes.

Some months later, an internal audit pulls every override from that quarter, because a run of overrides on one detector is worth investigating. Without a decision record, what exists is a ticket saying the transfer was approved, and a log line saying a control fired. Neither answers the real questions: which policy version produced the hold, whether it has since changed, or whether this override was rare that quarter or routine.

With the record, the answers sit in the fields themselves. policy_version: 2026-03-11.2 lets the audit load that exact policy, not whichever is deployed now, and confirm the rule that mapped this band to require_approval did fire as designed. detectors_run shows two other detectors cleared on the same request, answering “was this screened for anything else” without anyone having to ask the team that built it. override carries the approver’s name, the timestamp, and the stated reason, turning this one case from an anecdote into data: the audit can count how many overrides carried a genuine basis against how many were rubber-stamped, because the distinction was written down at the time, not reconstructed later.

When not to use it

The stakes here are specific to the record, not to the blueprint it comes from. Skipping the record is not a decision you can revisit later the way you can turn on stricter detection or add a policy rule. If a chokepoint enforced an action and wrote nothing but an outcome, no later audit recovers which rule fired, at what threshold, against which policy version, because none of it persisted anywhere else. The deferral, once made, is permanent for every decision it covers.

That asymmetry is the reason to be honest about where deferring is legitimate, rather than treating the record as something to add whenever convenient. A chokepoint whose policy has never differentiated by category or band, where the only rule that has ever fired is the bare default, has no enforcement decision to replay yet, so building the full pipeline is overhead spent ahead of the need. The moment a policy starts differentiating, even to allow the near band on purpose while blocking the one above it, full recording earns its keep, because that differentiated allow is exactly what Baseline and Floor needs to measure later. A pilot with no differentiated policy yet, with no regulator or customer able to question it, can name that gap and defer it, the same way an internal-only chain can defer the caveat machinery Attenuating Delegation asks for.

What that pilot cannot do is carry the deferral past the point where the policy starts mapping to an irreversible action, or a human starts overriding a block with a reason that matters. The moment either happens, the record has to already be running, since it cannot run backward over decisions already made. An institution that waits for its first real audit request before it starts writing decision records has already lost every decision that audit wants to see.

Split the Log argues the three-way separation this pattern only assumes: a decision record, telemetry, and a content archive answer different questions for different audiences; that case belongs there, not here.

Approval Gate is where the override field gets populated, and where decision_source earns its keep, separating a human’s own approval from a timeout that only looks like one. Either is a decision in its own right, and the record keeps it from evaporating into a ticket nobody can query months later.

The Expiring Exception depends on this pattern working. Confirming an exception expired on schedule, rather than quietly becoming permanent, needs a record of when it was granted, under which policy, and by whom.

Attenuating Delegation names this pattern as the only way to confirm, once a chain is too long for anyone to hold in their head, that no hop quietly re-granted authority an earlier hop had removed. The principal field is what makes that chain checkable at all.

Baseline and Floor needs exactly the near-threshold allow records described above: full detail on the below-threshold band, not just on the actions that fired, kept over time to measure whether recall is drifting.

Specified in

Sources

  1. (2026) Least privilege for AI agents: Identity, access, and tool binding