Leadership

Recording an Approval as Structured Data: Who, When, Against What

Antonio Evans9 min read

A clean isometric diagram on a light background.
Key points

Key points

  1. 1An approval record must name the exact artifact by digest, not the branch or the ticket, so a force-push cannot silently reuse it.
  2. 2It must carry the policy version that required the check, so a later policy edit does not retroactively validate or invalidate the decision.
  3. 3It must state who approved, the scope of their authority, when the approval was given and when it stops being valid.
  4. 4It must be written to an append-only log and referenced from the commit that consumed it, so it survives rewriting and can be replayed during an incident.

Why a chat message is not a record

Approvals usually start in chat. Someone asks whether a change can ship, someone else says go ahead, and the work proceeds. That exchange carries real authority in the moment and almost nothing afterward.

Test it against the questions an auditor or an on-call engineer will eventually ask. Which exact artifact was approved? A message names a branch, a pull request number or a screenshot. None of those identify content: a branch can point at different bytes minutes later, and a pull request can be force-pushed. What rule required the check? If the answer is "the checklist we used at the time" and the checklist has since been edited, the record no longer shows whether the approval satisfied the policy then in force. Was the approver entitled to approve this specific thing? Is the approval still valid? Which commit consumed it?

The failure is structural, not a discipline problem. A message is a statement about a conversation, not a statement about an artifact. There is a sharper reason to care now: autonomous agents hold write permissions and chain tool calls across systems, so one run can touch a database, a repository and a messaging tool in sequence per this agent blast radius analysis. When the requester is a process, there is no conversation for the approval to live in. The record is the only evidence a human authorized the specific action.

The six fields every approval record needs

Treat all six as mandatory. A record missing any one of them fails the test above.

1. Run or job identifier. This names the process that asked: a CI job id, an agent run id, a workflow execution id. It must be unique and stable for the life of the run. Two runs producing identical diffs are still two requests, and conflating them loses the distinction.

2. Artifact identity. The SHA-256 digest of the exact bytes approved, or the commit SHA plus a diff hash when the artifact is a change. This field makes everything else useful, and it is the one most often missing. SLSA provenance draws the same line: build inputs and outputs are identified by digests because the content behind a URI can change per the SLSA provenance specification. Provenance exists precisely because knowing who signed something does not tell you what was built as this SLSA and in-toto walkthrough explains.

3. Policy version. The identifier and version of the rule that required the check, for example prod-deploy@v7 or a hash of the policy document. This answers two different questions: was the approval sufficient under the rules of the time, and would it be sufficient today? After any policy edit, those answers diverge.

4. Reviewer and authority scope. Who approved, and what they were entitled to approve. Authority is not identity. A reviewer may hold a release-manager role that covers production deploys but not schema migrations, and recording only a user id invites the question of what that person could certify that day. The two-person rule makes the point well: the control only works when approvers are independent and empowered to reject, not merely names on a list per this glossary entry.

5. Timestamp and expiry. When the decision was made and when it stops being valid. Expiry matters more than teams expect. An approval granted Tuesday for a diff reviewed Tuesday should not authorize a Friday deploy, because the base branch advanced and the reviewer saw a different context. Short windows make stale approvals visible.

6. Commit reference. The identifier of the commit, deploy or run that consumed the approval. This closes the loop; without it, the approval and the deployment have no provable link.

An optional seventh field is worth adding: an evidence digest covering the diff, test output and policy evaluation the reviewer actually saw, so you can later prove the review matched what shipped.

FieldQuestion it answersWhat breaks without it
Run or job idWhich process asked?You cannot reconstruct the request from logs alone
Artifact digestWhat exactly was approved?Force-pushes and rebuilds silently invalidate the record
Policy versionWhich rule required this?You cannot judge sufficiency after a policy change
Reviewer and scopeWho approved, with what authority?Out-of-scope and self-approval go undetected
Timestamp and expiryWhen, and is it still valid?Stale approvals authorize changed work
Commit referenceWhat consumed the approval?Approval and deployment cannot be linked

A schema you can ship today

The following is a JSON schema in spirit rather than a formal specification. It is small enough for one table row and one log line.

{
  "approval_id": "apr_7f3c...",
  "run_id": "ci-run-88214",
  "artifact": {
    "kind": "diff",
    "tree_sha": "4b1e9a...",
    "diff_sha256": "c0ffee...",
    "base_ref": "main@9d2a11..."
  },
  "policy": { "id": "prod-deploy", "version": "v7", "digest": "sha256:ab12..." },
  "reviewer": {
    "subject": "user:6612",
    "role": "release-manager",
    "scope": ["prod-deploy", "rollback"],
    "independent_of_author": true
  },
  "decision": "approved",
  "decided_at": "2026-03-02T14:07:11Z",
  "expires_at": "2026-03-03T14:07:11Z",
  "commit": null,
  "consumed_at": null
}

Two choices need explaining. First, base_ref pins the base commit alongside the diff hash: the same patch applied to two bases produces two different results, so a diff hash alone is ambiguous. Second, commit and consumed_at start null and are filled by the deploy step. That is deliberate. The record exists before use, so a failed deploy leaves an approval granted but never consumed, which is exactly what an incident review needs to know.

The reviewer block carries independent_of_author because self-approval is the most common way a review control quietly stops working. If the pipeline does not check that flag, the flag is decoration.

Store approvals in an append-only log

An approval is only trustworthy if the system cannot quietly edit it. Use an insert-only table with no update or delete grants, or an append-only event stream. Corrections are new records referencing the old one (revokes: apr_7f3c...), never in-place edits.

This mirrors how distributed systems have long handled durable decisions. Two-phase commit works because each participant logs its vote and the final decision before acting, so a crash mid-protocol still leaves a recoverable record of what was promised per Martin Fowler's description of the pattern. The prepare phase is a promise; the log is what makes the promise survive a restart. An approval is a promise of the same kind, and the log is what makes it survive a force-push.

Practical rules:

  • Write the approval before execution. An approval recorded after the deploy is a receipt, not a gate.
  • Hash-chain each entry to the previous one so removal or reordering of history is detectable.
  • Never delete on artifact deletion. Retain records for the audit period even after the branch or repository is gone. SOC 2 Type II evidence must show a control operated across the whole period, which is impossible if records are pruned mid-period per this guide to valid SOC 2 evidence.
  • Keep each record self-contained so one row answers the question without chasing other systems.

If approvals span services, the two-phase idea generalizes: ScalarDB exposes a two-phase commit interface specifically so a transaction can span microservices with a coordinator and participants per the ScalarDB documentation. You do not need distributed transactions to record approvals, but you need the same discipline of agreeing on artifact identity before acting.

Consumption. A deploy job should refuse to run unless a valid record matches the artifact it is about to ship. Compute the digest of what you are deploying, query the log for an unconsumed record with that digest, unexpired, under the currently active policy version, and with a reviewer whose scope covers the action. If exactly one matches, proceed and append a new entry with the commit reference and consumption timestamp. If zero or many match, fail closed and say why. Two valid approvals for one artifact usually means something went wrong upstream, and the right response is to investigate rather than pick one.

Point the commit back at the approval with a trailer or deploy metadata field such as Approval-Record: apr_7f3c..., so anyone reading history can jump to the evidence. When the artifact is a model output or agent run, the same field goes in the run metadata, the same idea as attaching a provenance attestation to an artifact.

Querying during an incident. The value shows up at the worst moment. The queries you should run without writing code:

Incident questionQuery
What shipped in this window?Approvals where consumed_at falls in the window
Who approved the change that broke checkout?Match on artifact.diff_sha256, read reviewer and scope
Did anyone approve out of scope?Reviewer scope does not cover the action
Are we shipping on stale approvals?consumed_at later than expires_at (should be zero)
Did policy change mid-incident?Policy versions in force at each decided_at

The last query is the one almost nobody can answer and often the decisive one. If a policy tightened two days before an incident, approvals granted under the old version may have been legitimate then and inadequate in hindsight. Distinguishing those cases is the difference between "we followed our process" and "our process was wrong," and only a stored policy version tells them apart. Sweep in the other direction too: a compromised account or a confused reviewer probably approved more than one thing.

Reversing. Approvals are sometimes wrong. Reversal has three parts: append a revocation record referencing the original approval_id with a reason, revert the artifact and record the reversal commit in that entry, then audit the blast radius by querying everything else approved under the same policy version or by the same reviewer in the same window.

Reversibility of the action itself still matters. Reading wrong records is recoverable; deleting them, sending an external notification or charging a card is not per this analysis of bounding worst-case agent impact. The record cannot make an irreversible action reversible. It can tell you quickly which irreversible actions ran under the same flawed approval, which is the next best thing.

A worked example with numbers: a policy requires release-manager approval with a 24 hour expiry. Over a month the pipeline logs 400 approvals. A query finds 380 consumed inside the window and 20 consumed late, all by the same two reviewers during a holiday stretch. That is a 5 percent stale-approval rate concentrated in a known window and a known pair of people. Without the record the finding is invisible; with it, you have a target (shorter expiry plus calendar-aware escalation) and a measurable before-and-after.

Common failure modes and where agents change things

A checklist drawn from the patterns above:

  • Approving a branch instead of a digest. Compute the digest at approval and deploy time; refuse on mismatch.
  • Storing approvals in the issue tracker. Tickets are editable and deletable, which defeats the purpose.
  • Recording the reviewer without scope. Capture role and scope, validate at consumption.
  • No expiry. Set a default per policy and treat late consumption as a failure.
  • No commit link. Require the deploy step to write the consumption record before declaring success.
  • Approval recorded after execution. Make the gate synchronous, or asynchronous but strictly before execution.
  • No revocation path. Document and rehearse the append-only revocation entry before you need it.

One organizational failure deserves its own line: approval fatigue. When the same small group approves several times a day, the control becomes procedural rather than protective per this glossary entry. A structured record makes the pattern visible. If a query shows 90 percent of approvals coming from three people with a median request-to-approval time under a minute, the review is not doing its job, and chat records cannot even show you that.

Agents change the requirements in two ways. The run id becomes the primary identity of the request, because there is no human on the requesting side. And the artifact digest becomes non-negotiable, because an agent can summarize an intent and then do something slightly different. Dry-run patterns exist for this: HubSpot requires a completed dry run before an agent-driven workflow update or delete proceeds, and hands back a digest that must be passed to make the change happen per HubSpot's developer blog. Approval gates also cover what permission scopes cannot: OAuth answers whether an agent may call a tool at all, not whether this specific call with these arguments should run now, and that gap is where write operations fail silently per this human-in-the-loop guide.

For teams running both human approvals and agent runs, the natural next step is to build the record once and have both kinds of requester write to it. If your model calls and your agent runs share one surface, Quanta Inference gives you a single OpenAI-compatible key for text, image, video, audio and embedding models, so the run id and artifact digest can be captured where the call is logged.

FAQ

FAQ

Is a pull request approval in a code host not already a record?

Partly. Code hosts store a reviewer, a timestamp and often a commit SHA. They usually do not store the policy version that required the review, the scope of the reviewer's authority, an expiry, or a link to the deploy that consumed it. Treat it as a building block and export it into your own log.

How do I handle approvals for artifacts I cannot hash, like a database migration?

Hash what you can: the migration script, its parameters and the target schema version. For unhashable steps, record a dry-run output digest, the pattern HubSpot uses to require a preview before an agent-driven change per HubSpot's developer blog. The principle is that the approval must bind to something a reviewer actually saw.

Does this replace a change advisory board or ticketing system?

No. A record describes one decision about one artifact, while a board governs whether a class of change should happen and a ticket tracks the work. The approval record is evidence that the gate was passed, linked from the ticket rather than replacing it.

What expiry window should a production deployment approval use?

Set it per risk. A low-risk config change might stay valid for a day; a schema migration or production write might be valid for hours. The rule that matters is that expiry should be shorter than the time in which the reviewed context can meaningfully change. If your base branch moves hourly, an approval outliving the base commit is stale by definition.

How do I convince a team this is worth the extra fields?

Show them one incident query. Time how long it takes to answer what was approved, by whom, under which rule, and what consumed it, from current records. If that takes more than a few minutes, the six fields pay for themselves the first time, and teams running agents with write access adopt it fastest because the requesting side has no memory to fall back on.

Sources

Sources

  1. AI Agent Blast Radius: How Chained Tool Calls Expand It
  2. Build: Provenance
  3. Artifact Provenance and Attestations: From SLSA to in-toto
  4. What Is Two Person Rule? Definition & Examples
  5. Two-Phase Commit
  6. What counts as valid evidence in SOC2 Type II audits?
  7. Transactions with a Two-Phase Commit Interface
  8. Agent Blast Radius: Bounding Worst-Case Impact Before ...
  9. Using dry run to change workflows safely with Agent CLI
  10. Human-in-the-Loop Tool Calling: Approval Gates for AI ...
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