Business

How to Write a Decision Record for an AI That Affects People

Antonio Evans11 min read

A clean, structured form on a digital screen, with labeled rows: 'Reason code', 'Reviewer', 'Policy version', 'Appeal path'.
Key points

Key points

  1. 1A decision record is the structured, append-only entry written before an AI decision about a person takes effect, and it should be readable by someone who was not there.
  2. 2The minimum schema is decision ID, timestamp, subject reference, system and policy version, a reason code from a controlled list, the reviewer's identity and role, evidence references, the outcome, and the appeal path with a deadline.
  3. 3Retention should be set per decision class, not per system, using the longest applicable legal minimum plus a documented business need.
  4. 4The value of the whole exercise is that the appeal answer becomes retrieval, not investigation: the record already holds the reason, the authority, and the next step.

Why the explanation keeps going wrong

Most teams can reconstruct what an AI system did. Very few can reconstruct why a specific decision about a specific person was allowed at a specific moment, under a specific policy, with a specific person accountable.

Traces, application logs, and prompt transcripts sit side by side in most observability stacks and none of them answers that question. A trace shows latency, retries and spans. A tool log shows a request and a response. Neither captures business intent or the policy verdict that permitted the action, as this decision-log engineering guide points out when it separates traces from decision records (AI agent decision logs).

The second failure mode is subtler. Even when a log exists, it is written after the fact, by the runtime that did the work. That means a crashed worker, a timeout, or a duplicate retry can leave the record saying nothing, or saying "success" when the outcome was unknown. Writing the planned decision first and appending the verified outcome second fixes both, and it gives you a durable idempotency key as a side effect.

The third failure is regulatory rather than technical. Human oversight that is nominal does not count as oversight. A reviewer who lacks the information, authority or time to change the decision does not satisfy a meaningful-review requirement, and regulators are treating shallow review as equivalent to no review at all (Kiteworks on human in the loop). Your record is the only durable evidence that the reviewer could have changed anything.

The schema: eleven fields that make the answer boring

Design for one test: could a colleague who was absent reconstruct the reason, the authority and the result from this record alone, without opening the full prompt or the customer's file? If not, add a reference. If it needs the entire raw input, redesign the workflow so the record can cite scoped, access-controlled evidence instead.

FieldTypeWhy it existsCommon mistake
decision_idUUID, immutableJoins every related eventReusing the request ID, which can collide on retries
occurred_atISO 8601 UTCOrders events, starts clocksLocal time, or time of writing rather than time of decision
subject_refOpaque pointerAddresses the person without copying their dataEmbedding the applicant's name and SSN in the log
decision_classEnumDrives retention and review rulesOne class for all decisions, so everything inherits the longest retention
system_versionHash or build IDTells you which model or ruleset answeredA friendly model name that resolves to several builds
policy_versionVersioned stringAn answer legal under one policy may be blocked under the next"Current" or a floating tag
reason_codeControlled listMakes appeal answers automatic and testableFree-text explanations that drift from the model
reviewerID, role, modeProves who authorized, or that it was automatedRecording a queue name instead of a person
evidenceArray of refs plus digestsLets an investigator fetch exactly what was seenStoring full prompts and PII in the log
outcomeVerified, rejected, unknownSeparates attempted from confirmedWriting only on success
appealPath, deadline, stateTurns a complaint into a lookupProse buried in a policy wiki

Two fields deserve more than a table row.

reason_code is the load-bearing field, and a controlled list is the only version that scales. A consumer-lending notice workflow illustrates the failure precisely: a code map turned model reason code R14 into the sample-form phrase "insufficient credit" while the model actually scored length of credit history, and by the time anyone noticed, the same drift had already reached hundreds of letters (Botminds on adverse action automation). When the code list, the code map, and the policy version move together and are stamped on the record, that class of error becomes a version comparison rather than an audit finding.

reviewer needs to capture the oversight mode, not just a name. Human in the loop means the person authorizes before execution; human on the loop means they monitor a population and intervene when patterns look wrong; over the loop means retrospective audit only (Databricks on human in the loop). Storing which mode applied, and the person's role, is what lets you argue later that the review was real rather than available.

A minimal shape:

{
  "decision_id": "d-01J8ZQ4K7",
  "occurred_at": "2026-10-14T09:12:03Z",
  "decision_class": "customer_credit_line_change",
  "subject_ref": "party:9f2c...",
  "system_version": "sha256:4b1e...",
  "policy_version": "credit-policy-7.2",
  "reason_code": "R14_LENGTH_OF_HISTORY",
  "reviewer": { "id": "emp-4471", "role": "credit_analyst", "mode": "human_in_the_loop" },
  "evidence": [{ "kind": "bureau_report", "ref": "bureau:77aa...", "sha256": "..." }],
  "outcome": "verified",
  "appeal": { "path": "portal/reconsideration", "deadline": "2026-11-13" }
}

Note what is not there: no chain of thought, no raw prompt, no copy of the bureau report. The record cites evidence and describes the decision. It does not archive reasoning traces, which are expensive, sensitive, and not a dependable audit artifact.

Reason codes: the field that writes the explanation for you

If you get one field right, make it this one.

A reason code is a stable identifier for one specific, defensible explanation of a decision. It is not a model feature name, not a score contribution, and not a sentence. It is the join key between the model's behavior, the policy's justification, and the letter or message the person receives.

The regulatory logic is concrete. Under Regulation B, a creditor generally must give an applicant a statement of specific reasons for adverse action (CFPB, 12 CFR 1002.9). Generic phrases fail that test. "Incomplete application" is acceptable only when the file truly lacked information the creditor needed to decide; if the file had enough to decide, the notice must list the actual reasons for denial (CrossCheck Compliance on adverse action notices). Specificity is a record-design problem before it is a writing problem.

So build the code list under three rules:

  1. Every code maps to one plain-language sentence per audience (applicant, candidate, internal reviewer) and one internal justification.
  2. Every code is versioned with the policy that authorizes it, and retired codes are retained so historical records stay interpretable.
  3. Every code is testable: a unit test asserts that a given scored factor produced the expected code, so a model change that shifts meaning fails CI rather than shipping letters.

When that exists, the first question in an appeal ("why was I declined?") resolves to a template lookup keyed on reason_code plus policy_version. That is the boring answer you want.

Reviewer, authority, and the two-phase write

Record the decision before the side effect, then append the outcome. This is the difference between a log and a ledger.

Here is the shape with verification built in:

async function decide(input: ApplicantInput) {
  const d = await evaluate(input);            // typed facts + deterministic policy
  const record = await decisions.insert({ ...d, outcome: "unknown" });

  if (d.verdict !== "allow") return record;    // hold, block, or route to review is still a decision

  const result = await core.applyChange({
    subject: input.subjectRef,
    expectedVersion: input.version,
    idempotencyKey: record.decision_id,
  });

  return decisions.appendOutcome(record.decision_id, {
    state: result.version === input.version + 1 ? "verified" : "unknown",
    resultRef: `core-event:${result.eventId}`,
  });
}

Three properties follow. A crash between insert and apply leaves a durable record with state unknown, so reconciliation can find it. A duplicate retry reuses the idempotency key, so the person is not double-charged or double-rejected. And a hold verdict is itself a recorded decision, which matters because regulators increasingly ask about the decisions that were stopped, not only the ones that went through.

For the review phase itself, make approval an appended event rather than an edit to the original record. The reviewer sees the task summary, evidence references they are entitled to open, policy reason codes, the expected change, and an expiry. Their approval is a new immutable entry that names them, their role, and the time. If the review times out, that is also an event. Silent escalation to auto-approval is the single most common way a "human in the loop" claim collapses under examination.

Under GDPR Article 22, a person has protections against decisions based solely on automated processing that produce legal effects or similarly significant effects, and where the exceptions apply, the controller must implement safeguards that include the right to obtain human intervention, express a point of view, and contest the decision (Article 22 GDPR). The ICO's guidance turns this into an operating checklist: give people information about the processing, provide simple ways to request human intervention or challenge a decision, and carry out regular checks that the system works as intended (ICO on automated decision-making).

That maps to four record fields that must be externally renderable: the reason code with its plain-language sentence, whether a human authorized the decision and in what mode, the appeal path, and the appeal deadline. Everything else (evidence digests, internal model versions, reviewer employee ID) stays internal.

Two design rules follow. First, the external view is a projection, not a copy: render it from the record at request time so it always reflects the policy version in force when the decision was made. Second, keep the appeal deadline in the record, not in a downstream ticketing system. If the notice clock and the record clock are separate, they will diverge, and you will discover the divergence in a complaint rather than a dashboard.

Retention: set it per decision class

Retention is where most programs default to a single number and inherit a decade of unnecessary personal data. Derive it instead.

The general pattern in audit guidance is to retain records long enough to support after-the-fact investigation, with the specific duration left to organizational policy and applicable law; sector rules supply the floor. PCI DSS v4.0 requires at least 12 months of audit log retention with three months immediately available, HIPAA's security rule requires audit controls without fixing a period, and GDPR Article 30 requires records of processing activities, which can extend to AI decision logs where personal data is involved (Trussed on policy decision log retention). Where several regimes apply, keep the longest minimum.

Then layer the decision class on top:

Decision classTypical floor driverPractical retentionRationale
Customer credit or pricingNotice and recordkeeping rules, plus complaint windows3 yearsCovers the notice clock, the complaint cycle, and a full annual review
Candidate screening or rejectionLocal employment discrimination filing windows2 yearsAligns with the window in which a claim is most likely
Support or service entitlementContract and consumer protection2 yearsMatches the dispute and warranty period
Access or fraud blockSecurity incident review1 year, 3 months hotMirrors the log-retention convention above
Low-risk personalizationPurpose limitation90 days or aggregate onlyIf it cannot affect the person materially, do not keep the individual row

Two retention obligations pull in opposite directions and must be reconciled explicitly. Privacy law pushes toward deletion (storage limitation, purpose tracking, and in the EU and UK the storage-limitation principle described in regional retention guidance (Scrut on data retention by region)). Audit and litigation obligations push toward keeping the structured record. The resolution is to delete the payload and keep the record. Evidence references expire with the underlying data; the decision row, reason code, policy version, reviewer and appeal state persist for the retention period.

There is a standards effort that takes this seriously. The Agent Audit Trail draft specifies hash-chained, tamper-evident records with pre-execution recording, deny reason codes, and tombstone-based deletion compatible with GDPR erasure requests, so the chain verifies even after personal data is removed (IETF Agent Audit Trail draft). You do not need to implement the whole draft to borrow the two ideas that matter: record before the action, and delete content while preserving proof.

Storage, integrity, access, and review cadence

Store the record in an append-only store, separate from the application database, with the decision-producing component as the authoritative writer. NIST's access-control architecture separates the policy decision point from the policy enforcement point, and decision-record guidance applies that split to AI systems by treating the component that produced the decision as the source of the log entry rather than the agent runtime (Trussed, citing NIST SP 800-162 and AU-3).

Concretely: write once, then append, with no updates to existing rows and only new outcome and review events. Hash-chain consecutive records so tampering is detectable, with periodic anchoring to an external timestamp so a rewritten chain cannot hide. Keep subjects and reviewers as opaque identifiers and join to identity in a system with its own access control. Apply least privilege by role so a reviewer sees the evidence, an appeals agent sees the reason code and policy version, and an auditor sees everything read-only. Encrypt at rest and in transit, and log access to the record store itself.

This is also where observability discipline pays off. Keeping task and trace identifiers on the record lets you join to the performance and latency data you already collect, without duplicating prompts or personal data into the decision store (Techlusion on agent observability).

A record you never read is a cost, not a control. Set a cadence by decision class, and make the review produce artifacts rather than impressions.

CadenceWhat is reviewedWho signsOutput
Every decisionReason code is in the active list; reviewer present if mode requires itAutomated check in CIBlocked deploy on failure
DailyHolds, timeouts, and unknown outcomes older than a thresholdOps leadReconciliation ticket
WeeklySample per class: appeals opened, outcomes reversedCompliance analystTrend note, code-list backlog
MonthlyOverride rate by reviewer and reason code; clock performanceModel owner plus complianceDrift or training action
QuarterlyPolicy version changes, code retirements, retention expiry runGovernance groupRecorded decision on the schema or list

The one metric worth tracking above all others is the reversal rate by reason code. It tells you where the model and the explanation disagree, which is exactly where your organization will lose an appeal. A decision rule for what belongs in the record at all: log the moment the system crosses from interpretation into a consequential branch. That includes selecting a workflow, classifying a request into a risk tier, changing a record, sending a message, charging money, denying an action, escalating to a human, and declaring a task complete. Do not log retrieval chunks, retries, or draft sentences; those belong in traces, linked by ID.

Getting the mechanics right, in order

A short sequence that avoids the most common rewrites:

  1. List every decision class that affects a person, then rank by reversibility and regulatory exposure.
  2. For the top class, write the controlled reason-code list with plain-language sentences before touching the schema.
  3. Version the policy and the code list together; stamp both on the record.
  4. Implement the two-phase write, with unknown as a first-class outcome.
  5. Separate the internal record from the external projection, and put the appeal deadline in the record.
  6. Set retention per class from the table above, with payload deletion and record persistence.
  7. Stand up the weekly sample and the monthly override review before you scale volume.

If you are building this on top of several model providers, the plumbing is where effort hides. Quanta Inference exposes text, image, video, audio, and embedding models through one OpenAI-compatible API key, prepaid and priced in US dollars (https://quantaintelligence.ai/inference), which keeps the version stamps in your record pointing at a single integration rather than one per vendor.

FAQ

FAQ

What is the difference between a decision log and a decision record?

A log is a stream of events; a record is the structured, per-decision entry designed to be read by an auditor, an appeals agent, or a regulator. The record should be derivable from the stream, but it is modeled around the question "why was this allowed, and who authorized it," not around latency and request diagnostics.

Should the record include the model's chain of thought?

No. Private intermediate reasoning is not a dependable audit artifact, and storing it duplicates sensitive content under a second retention regime. Record the policy verdict, the reason codes, and scoped evidence references instead, and keep the full prompt only in a sampled, redacted observability store.

How long should we keep decision records about customers and candidates?

Set it per decision class and pick the longest applicable floor. A common practice is three years for credit and pricing decisions and two years for hiring decisions, with payload deleted earlier and the structured record retained, so you can still prove what was decided after the underlying evidence has been erased.

Does a reviewer have to see the whole case file?

They have to be able to change the outcome. That means access to the evidence the decision relied on, the reason codes, the policy version, and enough time to act. A record that shows a reviewer's name but no ability to see or override the basis is evidence of nominal oversight, which is not the same as compliant oversight.

How do we handle erasure requests without breaking the audit trail?

Keep the identifiers and proof, remove the content. Store evidence as references and digests rather than copies, expire the references when the source data is deleted, and use hash chaining with tombstone entries so the chain still verifies after erasure. That preserves accountability while honoring storage-limitation and erasure obligations.

Sources

Sources

  1. AI Agent Decision Logs: Record Evidence Without Storing Chain of ...
  2. Human in the Loop: AI Compliance and Oversight ...
  3. Adverse Action Notice Automation - Botminds AI
  4. What is Human-in-the-Loop (HITL)?
  5. § 1002.9 Notifications. | Consumer Financial Protection Bureau
  6. Getting Adverse Action Notices Right - CrossCheck Compliance
  7. Automated individual decision-making, including profiling
  8. Rights related to automated decision making including ...
  9. Policy Decision Log: Format, Fields, and Retention - Trussed AI
  10. Data Retention policy & best practices by region
  11. Agent Audit Trail: A Standard Logging Format for Autonomous AI Systems
  12. Observability for AI Agents, Logs, Traces and SLOs You Need
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles