Approval Step
Table of contents
A workflow primitive: a single binding of a required approval to a named approver, for a specified subject and scope, with a lifecycle from submission through decision. Each approval step has an opaque immutable id; the subject, approver, scope, submitter, and timestamps are immutable properties set at submission. Four states — Pending (the initial state) plus three terminal states (Approved, Rejected, Withdrawn). All three terminal states are absorbing. The named approver is the only actor authorized to transition Pending to Approved or Rejected; the submitter is the only actor authorized to transition Pending to Withdrawn; delegation is a composing concern.
Intent
Many actions in regulated systems require explicit human authorization before they may proceed. A financial journal entry exceeding a materiality threshold must be approved by a controller before it posts. A clinical trial protocol deviation must be approved by the principal investigator before the deviant procedure occurs. A pharmaceutical batch release must be approved by a qualified person under 21 CFR Part 211. An engineering change order must be approved through a release chain before it reaches production. In every case, the structure is the same: something is submitted for approval, a named actor reviews it, and the outcome — approved, rejected, or withdrawn by the submitter — is an auditable record that external evaluators rely on as evidence that the required control existed and operated.
Approval Step is the specification of that structure. It records the gate. It records who the gate was for. It records the outcome, the actor who decided it, and when. It guarantees these records are present and immutable. Cryptographic protection of those records against post-hoc modification — the bar for court-admissible evidence of control operation under SOX §404 and FDA Part 11 — is added by composition with Tamper Evidence; this atom does not provide it alone.
The atom is structurally distinct from three adjacent concepts, and the distinctions are load-bearing:
Approval Step vs. Permissions. Permissions governs standing authorization — a persistent grant binding a subject to a set of permitted action scopes, checked at every relevant action invocation, active until revoked. An Approval Step governs transient decision authorization — a one-time gate applied to a specific subject (a document, a transaction, an action instance) that produces a terminal outcome and is then closed. A Permissions permitted check answers “may this actor generally do things of this kind?” An Approval Step answers “has this specific thing been approved by the specifically named actor for this specific scope?” A controller with Permissions to approve journal entries still needs an Approval Step record for each entry that was actually approved — the Permissions grant says they are allowed to approve; the Approval Step says they did approve this one. Both atoms are present in a conforming SOX deployment; neither replaces the other.
| Approval Step vs. Assignment. Assignment governs responsibility binding — an active binding that a task or work item is the named actor’s to work on, with a lifecycle of its own (Pending → Active → Complete | Recalled | Expired). Assignment answers “who is responsible for doing this work?” An Approval Step answers “has this thing been approved by the named actor?” These often coexist without overlapping: a document may be assigned to an author (Assignment) and simultaneously submitted to a reviewer for approval (Approval Step). Assignment does not produce a decision record; Approval Step does not track work ownership. Confusing them produces either a spec that cannot record the approval decision or one that loses track of who owns the work. |
Approval Step vs. the eventual Workflow / State Machine atom. The forthcoming Workflow / State Machine atom (#9) is the general-purpose state machine engine: a named entity moving through a deployment-declared set of states via deployment-declared transitions, with the full transition history auditable. Approval Step is a specific kind of state machine whose states are fixed by this atom (Pending, Approved, Rejected, Withdrawn), whose transitions are approval-specific (submit, approve, reject, withdraw), and whose semantics are approval-specific (exactly one named approver; decision produces a terminal outcome that is itself a compliance record). The distinction matters because Approval Step is fully specified at the atom level — external evaluators know the states and their semantics without consulting a deployment configuration — while Workflow / State Machine is specified by deployment declaration. Multi-party approval chains that wire multiple Approval Step instances together belong to the Multi-Party Approval composition; chains where the approval state logic is itself configurable at deployment time belong to Stateful Workflow Execution (C10). This atom’s scope is the single gate.
This is a freestanding (can be specified without naming any other pattern) concept in the EOS (Essence of Software — Daniel Jackson’s framework for specifying software concepts as freestanding, composable units) sense. It carries its own state (the approval step record set), its own actions (submit, approve, reject, withdraw, read), and its own invariants (subject immutability, approver exclusivity, terminal absorption, decision completeness, concurrent step independence). Composing patterns add multi-party chains, access control, event logging, tamper evidence, and delegation.
Summary
Approval Step is the atom that records a single authorization gate: the binding of a named approver to a specific subject requiring their approval, for a declared scope of action, with a lifecycle that runs from submission through a terminal decision. Its job is to produce an auditable, immutable (unchangeable once written) record that a required approval gate existed, was presented to a specific named actor, and was either approved, rejected, or withdrawn — together with who made the decision, when, and why.
The structure is fixed by design. Each approval step has exactly one named approver (the only actor who may approve or reject it), one named submitter (the only actor who may withdraw it), one subject (the thing being approved — a document, a transaction, a protocol deviation), and one scope (the category of approval being requested — for example, financial:journal-entry:post or clinical-trial:protocol-deviation). Four states are possible: Pending (the initial state, gate is open), Approved (affirmative decision by the named approver), Rejected (negative decision by the named approver, with a required reason), and Withdrawn (the submitter retracted the request before a decision was made). All three terminal states are absorbing (no further transitions are possible once reached).
The atom’s core guarantee is decision attribution completeness: every Approved or Rejected step carries the deciding actor’s reference and timestamp, both immutable; every Rejected step additionally carries a required reason; every Withdrawn step carries the withdrawing actor, a required reason, and a timestamp. An anonymous decision, a blank reason on a rejected step, or a missing timestamp is a conformance failure. This makes Approval Step the structural form of a regulated control — when an SOX auditor asks for evidence that journal entries were approved by the right controller, or an FDA investigator asks who approved a pharmaceutical batch release, the answer must come from the records alone without recourse to developer testimony.
Approval Step is distinct from Permissions (which governs standing, reusable authorization for a class of actions) and from Assignment (which governs who owns a work item, not who approved it). It is also distinct from the forthcoming Workflow / State Machine atom, which provides a general-purpose engine with deployment-declared states; Approval Step is a specific kind of state machine whose states and semantics are fixed here and are fully interpretable by external evaluators without consulting any deployment configuration.
The most common uses are: SOX §404 financial control gates (controller approval before a journal entry posts), FDA 21 CFR Part 11 electronic signature requirements (named actor approval of batch release, protocol deviation), clinical trial oversight (investigator approval before a deviant procedure occurs), and engineering change control (approval chain before a change order reaches production). The atom is grounded (passed all required review passes and is stable enough to generate from) and is the first entry in atoms/workflow/.
Structure
Store instance model
The Approval Step atom operates against a named store instance. A store_name identifies the instance; multiple instances coexist in real systems — one per organization, department, or regulated domain, depending on deployment topology. step_id values are unique within a store instance; uniqueness across instances is a composing concern. The same subject_ref may appear in multiple simultaneous Approval Step records within the same store instance — one per required approval gate. Calls implicitly target a single routed instance; instance selection is a deployment-routing concern, not defined by this atom.
Identity model
Each approval step has an opaque, immutable, system-generated step_id — assigned on submit, never reused, never reassigned within the store instance. It must be a non-empty string sortable in lexicographic byte-order; this property is required for deterministic read ordering. The id is the step’s identity; the subject, approver, scope, submitter, reason, and timestamps are properties of the step, not its identity.
subject_ref is an opaque reference to the thing being approved — a document id, a transaction id, a work item id, a protocol deviation id. Set on submit, immutable. The atom does not validate that the subject exists or is in any particular state — subject_ref is the caller’s responsibility. Two approval steps covering the same subject have distinct step_ids; each is its own record with its own lifecycle.
approver_ref is an opaque reference to the actor required to approve. Set on submit, immutable. It is the authorization anchor: the only actor permitted to call approve or reject on this step is the one whose reference matches approver_ref. Empty or whitespace-only values are rejected at submission. Delegation — binding a different actor to step in for the named approver — is a composing concern, not a property of this atom.
submitter_ref is an opaque reference to the actor submitting the approval request. Set on submit, immutable. It is the attribution anchor for the submission decision; empty or whitespace-only values are rejected at submission.
scope is a non-empty string naming the kind of approval being requested — for example, "financial-journal-entry:post", "clinical-trial:protocol-deviation", or "change-order:release". Set on submit, immutable. The atom does not interpret scope semantics; it records the scope as an auditable field and uses it as a filter axis for read. Must contain at least one non-whitespace character.
reason is an optional non-empty string providing context for the approval request — a narrative description of what is being approved, a reference to the underlying business rule, or a summary of the deviation. Set on submit, immutable. Its absence is valid — some deployment contexts supply all context through subject_ref and scope. If supplied, it must contain at least one non-whitespace character.
Inputs
submitcalls from actors requesting approval — business process actors, automated workflow engines, change management integrations — each carrying a subject reference, approver reference, submitter reference, scope, optional reason, and optional explicit timestamp.approvecalls from the named approver, documenting the affirmative decision, carrying the step id, the deciding actor, an optional stated reason, and an optional explicit timestamp.rejectcalls from the named approver, documenting the negative decision, carrying the step id, the deciding actor, a required reason, and an optional explicit timestamp.withdrawcalls from the submitter, documenting that the request has been retracted, carrying the step id, the withdrawing actor (must matchsubmitter_ref), a required reason, and an optional explicit timestamp.readqueries from auditors, process operators, downstream workflow systems, and approval dashboards.
Actions
For optional parameters in submit, approve, reject, and withdraw, “supplied” means provided as a parseable value of the declared type. Null, missing, and empty (or whitespace-only) values are equivalent to “not supplied,” and the action’s documented default applies.
-
submit(subject_ref, approver_ref, submitter_ref, scope, reason?, submitted_at?) → step_id | rejected(invalid-request | storage-failure)— create a new approval gate. Assigns a freshstep_id, recordssubject_ref,approver_ref,submitter_ref,scope,reason(if supplied), andsubmitted_at(wall clock if not supplied; must not be in the future — a step cannot be submitted in the future). The step enters Pending state.subject_ref,approver_ref,submitter_ref, andscopemust each contain at least one non-whitespace character;reason, if supplied, must also contain at least one non-whitespace character — any violation isinvalid-request.storage-failureif the store write fails after all preconditions pass; nostep_idis issued and no record enters the store. -
approve(step_id, decided_by, reason?, decided_at?) → approved | rejected(invalid-request | not-known | not-pending | unauthorized | storage-failure)— record the affirmative decision and transition the step to Approved. Recordsdecided_by,decision_reason(if supplied; absence is valid for an approval — rejection requires a reason), anddecided_at(wall clock if not supplied; must not be in the future). All decision fields are immutable after the transition. Thestep_idparameter must contain at least one non-whitespace character (invalid-request); a null, empty, or whitespace-onlystep_idis malformed and rejected before any existence check is performed.decided_bymust contain at least one non-whitespace character (invalid-request). The resolveddecided_at— whether caller-supplied or wall-clock-defaulted — must be ≥ the step’ssubmitted_at; a value less thansubmitted_atisinvalid-requestregardless of how it was derived (this enforces Invariant 7 against clock-skew artifacts as well as caller-supplied backdated values).decided_bymust matchapprover_ref(unauthorized); the atom rejects approval from any actor other than the named approver.storage-failureleaves the step in Pending; the caller must retry. Rejection priority: malformedstep_id(invalid-request) →not-known→not-pending→ attribution/temporal (invalid-request) →unauthorized→storage-failure. -
reject(step_id, decided_by, reason, decided_at?) → rejected_outcome | rejected(invalid-request | not-known | not-pending | unauthorized | storage-failure)— record the negative decision and transition the step to Rejected. Requires a non-emptyreason; a rejection without a stated reason is not operationally meaningful and defeats the audit trail that regulated proceedings depend on. Recordsdecided_by,decision_reason, anddecided_at(wall clock if not supplied; must not be in the future). All decision fields are immutable after the transition. Thestep_idparameter must contain at least one non-whitespace character (invalid-request). The resolveddecided_atmust be ≥ the step’ssubmitted_at.decided_bymust matchapprover_ref(unauthorized). The success token isrejected_outcomerather thanrejectedto distinguish the action’s success result from the action’s own rejection path.storage-failureleaves the step in Pending; the caller must retry. Rejection priority mirrorsapprove: malformedstep_id(invalid-request) →not-known→not-pending→ attribution/temporal (invalid-request) →unauthorized→storage-failure. -
withdraw(step_id, withdrawn_by, reason, withdrawn_at?) → withdrawn | rejected(invalid-request | not-known | not-pending | unauthorized | storage-failure)— record the submitter’s retraction and transition the step to Withdrawn. Requires a non-emptyreason. Recordswithdrawn_by,withdrawal_reason, andwithdrawn_at(wall clock if not supplied; must not be in the future). All withdrawal fields are immutable after the transition. Thestep_idparameter must contain at least one non-whitespace character (invalid-request). The resolvedwithdrawn_atmust be ≥ the step’ssubmitted_at.withdrawn_bymust matchsubmitter_ref(unauthorized); withdrawal is the submitter’s act, not the approver’s.storage-failureleaves the step in Pending; the caller must retry. Rejection priority: malformedstep_id(invalid-request) →not-known→not-pending→ attribution/temporal (invalid-request) →unauthorized→storage-failure. -
read(query) → ordered_sequence_of_steps | rejected(invalid-query)— return steps matching the query, ordered bysubmitted_atascending, then bystep_idascending in lexicographic byte-order as a stable tiebreaker. Implementations must assignstep_idvalues in a format where string byte-order sort produces a total order (e.g., ULID, UUID v7, or zero-padded integer string). The supported filter axes are exactly:step_id,subject_ref,approver_ref,submitter_ref,scope,state, and time ranges onsubmitted_at,decided_at, orwithdrawn_at. A time range filter on any of those timestamp fields takes the form{after: <timestamp>, before: <timestamp>}with both sub-keys optional;afteris an inclusive lower bound andbeforeis an inclusive upper bound. A range carrying onlyafteris unbounded above; a range carrying onlybeforeis unbounded below; a range carrying both bounds the result inclusively on both ends. Filter keys are flat strings, not dot-notation paths. Any combination of supported axes is valid. A query supplying only astep_idreturns at most one step. A well-formed query matching no steps returns an empty sequence, not a rejection. A query with no filters returns every step in the store.A time range filter on
decided_atreturns only steps that carry adecided_atfield — i.e., Approved or Rejected steps. Pending and Withdrawn steps carry nodecided_atfield and are implicitly excluded from results whenever adecided_atfilter is present, regardless of whether astatefilter is also supplied. A time range filter onwithdrawn_atreturns only Withdrawn steps by the same rule. A query filtering on a timestamp field that a given state does not carry returns only steps of states that do carry it; this rule applies to any timestamp-absent-field combination, not only the enumerated examples here. Ascopefilter value matches only steps wherescopeequals the value exactly. The query{subject_ref: X, state: Pending}returns every Pending step covering a given subject — this is the operational check for whether a subject has an outstanding approval gate.Malformed-query rules (
invalid-query): astep_id,subject_ref,approver_ref,submitter_ref, orscopefilter value that is null, empty, or whitespace-only isinvalid-query. Astatefilter value that is not one of {Pending,Approved,Rejected,Withdrawn} isinvalid-query. A time range with end before start isinvalid-query. A query carrying an unrecognized filter key — any key outside the supported axes named above — isinvalid-query; an unrecognized key is rejected rather than silently ignored, because silent ignore would return a result set inconsistent with the caller’s intent.
Outputs
- For
submit: a freshstep_id, or a rejection. - For
approve: the outcome tokenapproved, or a rejection. - For
reject: the outcome tokenrejected_outcome, or a rejection. - For
withdraw: the outcome tokenwithdrawn, or a rejection. - For
read: a (possibly empty) ordered sequence of steps. Each step carries its full field set. Fields present on every step (any state):step_id,subject_ref,approver_ref,submitter_ref,scope,submitted_at,state. Optional field set at submission (independent of state):reason(present if supplied atsubmit, absent otherwise; immutable thereafter). State-specific fields:decided_by,decision_reason,decided_atare present on Approved and Rejected steps only;withdrawn_by,withdrawal_reason,withdrawn_atare present on Withdrawn steps only. An Approved or Rejected step carries all submission fields (includingreasonif it was supplied) and all decision fields simultaneously. A Withdrawn step carries all submission fields and all withdrawal fields.
State
Each approval step is in exactly one state:
- Pending — the approval gate (a named checkpoint that must be cleared before a workflow can advance) is open; no terminal decision has been made. Carries
step_id,subject_ref,approver_ref,submitter_ref,scope,submitted_at, andreason(if supplied). May be transitioned to Approved (by the named approver), Rejected (by the named approver), or Withdrawn (by the submitter). No other transitions are valid. - Approved — the named approver has affirmatively decided. Carries all submission fields plus
decided_by,decision_reason(if supplied), anddecided_at(all immutable from the momentapprovecompletes). Terminal; no further transitions. - Rejected — the named approver has negatively decided. Carries all submission fields plus
decided_by,decision_reason, anddecided_at(all immutable from the momentrejectcompletes). Terminal; no further transitions.decision_reasonis required on Rejected steps; it is optional on Approved steps. - Withdrawn — the submitter has retracted the request. Carries all submission fields plus
withdrawn_by,withdrawal_reason, andwithdrawn_at(all immutable from the momentwithdrawcompletes). Terminal; no further transitions.
Valid transitions:
submit(...)→ new step enters Pending- Pending → Approved (via
approve, by the named approver only) - Pending → Rejected (via
reject, by the named approver only) - Pending → Withdrawn (via
withdraw, by the submitter only)
No other transitions exist. A Pending step cannot be re-submitted as a new version; a fresh approval need requires a new submit call producing a new step_id. All three terminal states are absorbing.
Flow
- Submission. A controller determines that posting journal entry JE-2026-0441 requires senior finance approval under SOX §404 controls. Calls
submit(subject_ref: "je-2026-0441", approver_ref: "finance_director_chen", submitter_ref: "controller_morgan", scope: "financial:journal-entry:post")→step_id: "step-001". The step enters Pending. - Review. The finance director receives notification (via a composing notification workflow; out of scope here) and reviews the journal entry.
- Approval. The finance director approves:
approve("step-001", decided_by: "finance_director_chen", reason: "Reviewed and approved — posting authorized")→approved. The step is now Approved. The composing workflow system releases the journal entry for posting. - Audit query. A SOX auditor later queries
read({subject_ref: "je-2026-0441", state: Approved})and sees the step with full attribution: who submitted, who approved, when, and the stated reason. The control evidence is present in the records without recourse to developer testimony.
Alternatively, at step 3: the finance director finds a misclassification and rejects: reject("step-001", decided_by: "finance_director_chen", reason: "GL account 4120 is incorrect — should be 4130 per revenue recognition policy") → rejected_outcome. The step is now Rejected. The composing workflow routes the entry back to the controller for correction, who must submit a new approval step for the corrected entry.
Or: before step 3, the controller discovers the entry was submitted to the wrong approver and withdraws it: withdraw("step-001", withdrawn_by: "controller_morgan", reason: "Submitted to wrong approver — should route to tax_director for cross-border entries") → withdrawn. A new submit call creates step-002 with the correct approver_ref.
Decision points
-
At
submit—subject_ref,approver_ref,submitter_ref, andscopemust each contain at least one non-whitespace character;reason, if supplied, must also contain at least one non-whitespace character — any violation isinvalid-request.submitted_at, if supplied, must not be in the future (checked against the receiving node’s wall clock); a violation isinvalid-request.storage-failureif the store write fails after all preconditions pass; nostep_idis issued, no record enters the store. Rejection priority: field-validation (invalid-request) →storage-failure. -
At
approve— thestep_idparameter is checked first: if null, empty, or whitespace-only, the call isinvalid-request(the caller passed garbage, not a reference to a missing step). Ifstep_idis well-formed, the store is consulted:not-knownif no step with this id exists;not-pendingif the step is in Approved, Rejected, or Withdrawn state. If neither, attribution and temporal checks apply:decided_bymust contain at least one non-whitespace character (invalid-request); the resolveddecided_at— caller-supplied or wall-clock-defaulted — must not be in the future (the future-bound applies when caller-supplied; a wall-clock default is “now” by construction) and must be ≥ the step’ssubmitted_at. The≥ submitted_atbound applies to the resolveddecided_atregardless of how it was derived; this enforces Invariant 7 against clock-skew artifacts and caller-supplied backdated values. A violation isinvalid-request. Then identity check:decided_bymust matchapprover_refexactly (unauthorized).storage-failureleaves the step in Pending; the caller must retry. Rejection priority: malformedstep_id(invalid-request) →not-known→not-pending→ attribution/temporal (invalid-request) →unauthorized→storage-failure. -
At
reject— identical toapprovein structure, with one addition:reasonis required forreject(not optional as inapprove); a null, empty, or whitespace-onlyreasonisinvalid-request. All other checks and rejection priorities are identical toapprove. -
At
withdraw— thestep_idparameter is checked first as above. The store is consulted:not-known;not-pending. Attribution and temporal checks:withdrawn_bymust contain at least one non-whitespace character (invalid-request); the resolvedwithdrawn_atmust not be in the future and must be ≥ the step’ssubmitted_at(invalid-request). Then identity check:withdrawn_bymust matchsubmitter_refexactly (unauthorized).storage-failureleaves the step in Pending; the caller must retry. Rejection priority: malformedstep_id(invalid-request) →not-known→not-pending→ attribution/temporal (invalid-request) →unauthorized→storage-failure. -
At
read— every supplied filter value must be well-formed for its axis. Astep_id,subject_ref,approver_ref,submitter_ref, orscopefilter value that is null, empty, or whitespace-only isinvalid-query. Astatefilter value not in {Pending,Approved,Rejected,Withdrawn} isinvalid-query. A time range with end before start isinvalid-query. An unrecognized filter key — any key outside the supported axes — isinvalid-query; the spec rejects rather than ignores unknown keys. A time range filter on a timestamp field implicitly excludes states that do not carry that field, regardless of whether astatefilter is also present. A well-formed query matching no steps returns an empty sequence.
Behavior
- Steps are durable on success. Once
submitreturns astep_id, the step is in the store and will appear in subsequent reads. - Step submission is not idempotent. Two
submitcalls for the samesubject_ref,approver_ref, andscopecreate two independent steps with distinctstep_ids. For at-most-once semantics on submission, compose with Duplicate Prevention. - The named approver is the only actor who may decide.
approveandrejectare rejected asunauthorizedifdecided_bydoes not matchapprover_ref. There is no fallback approver, no escalation path, and no “any authorized actor” surface in this atom. These are composing concerns. - The submitter is the only actor who may withdraw.
withdrawis rejected asunauthorizedifwithdrawn_bydoes not matchsubmitter_ref. - Terminal states are absorbing. A step in Approved, Rejected, or Withdrawn state accepts no further transitions. There is no re-open, no re-activate, and no decision reversal surface. A fresh approval need requires a new
submitcall producing a newstep_id. - Multiple concurrent approval steps on the same subject are independent. A subject with two outstanding Pending steps requires two decisions. Deciding on step A (via
approve,reject, orwithdraw) does not affect step B. Whether a subject has any outstanding approval gates is answered byread({subject_ref: X, state: Pending}); if the result is non-empty, at least one gate is open. - Reads are repeatable; the step store is monotonic. The step store only grows —
submitadds records;approve,reject, andwithdrawtransition them. An unfiltered read att2 > t1returns every step visible att1plus any added in between. State-filtered reads are not monotonic: a step visible understate: Pendingatt1may appear under a terminal state att2if decided in between.
Feedback
- After
submit— a new Pending step exists;step_id,subject_ref,approver_ref,submitter_ref,scope,submitted_at, andreason(if supplied) are set and immutable. - After
approve— the step is now Approved;decided_by,decision_reason(if supplied), anddecided_atare set and immutable. All submission fields are unchanged. - After
reject— the step is now Rejected;decided_by,decision_reason, anddecided_atare set and immutable. All submission fields are unchanged. - After
withdraw— the step is now Withdrawn;withdrawn_by,withdrawal_reason, andwithdrawn_atare set and immutable. All submission fields are unchanged.
Each rejected action produces an observable refusal naming the failed precondition.
Invariants
-
Invariant 1 — Submission immutability. After a successful
submit, the fieldsstep_id,subject_ref,approver_ref,submitter_ref,scope,submitted_at, andreasonnever change, regardless of any subsequent action. -
Invariant 2 — Membership exclusivity. Every step known to the store is in exactly one of {Pending, Approved, Rejected, Withdrawn} at all times.
-
Invariant 3 — Terminal absorption. Once a step transitions to Approved, Rejected, or Withdrawn, no action transitions it further. All three terminal states are absorbing. The atom has no re-activate, re-open, or decision-reversal surface; a fresh approval need requires a new
submit. -
Invariant 4 — Approver exclusivity. Only the actor whose opaque reference matches
approver_refmay transition a step from Pending to Approved or Rejected. Any call toapproveorrejectwheredecided_bydoes not matchapprover_refis rejected asunauthorized. Delegation — binding a different actor to step in for the named approver — is not a property of this atom; it belongs to a composing delegation pattern. -
Invariant 5 — Submitter exclusivity. Only the actor whose opaque reference matches
submitter_refmay transition a step from Pending to Withdrawn. Any call towithdrawwherewithdrawn_bydoes not matchsubmitter_refis rejected asunauthorized. -
Invariant 6 — Decision attribution is complete for terminal steps. Every Approved or Rejected step carries
decided_bycontaining at least one non-whitespace character and adecided_attimestamp that is set. Every Rejected step additionally carriesdecision_reasoncontaining at least one non-whitespace character. Every Withdrawn step carrieswithdrawn_bycontaining at least one non-whitespace character,withdrawal_reasoncontaining at least one non-whitespace character, and awithdrawn_attimestamp that is set. An anonymous decision, a whitespace-only attribution string, a missing timestamp, or a Rejected step without a stated reason is a conformance failure — each defeats the audit trail that SOX control evidence and FDA Part 11 electronic signature requirements depend on. -
Invariant 7 — Temporal ordering. For every Approved or Rejected step,
decided_at ≥ submitted_at. For every Withdrawn step,withdrawn_at ≥ submitted_at. A step cannot be documented as decided or withdrawn before it was submitted. These constraints apply to the values persisted in the record, regardless of whether the timestamps were caller-supplied or wall-clock-defaulted; the Decision points forapprove,reject, andwithdrawenforce the bounds against the resolved values before any transition is committed. -
Invariant 8 — Submission attribution is complete. Every step, in any state, carries
step_id,subject_ref,approver_ref,submitter_ref, andscopeeach containing at least one non-whitespace character, and asubmitted_attimestamp that is set. Invariant 1 guarantees these fields are immutable; this invariant guarantees they are never blank or unset. An anonymous submission, a whitespace-only scope, or a missing timestamp is a conformance failure — it defeats the chain of custody that an external evaluator needs to reconstruct who requested the approval and what it covered. -
Invariant 9 — Concurrent step independence. Transitioning step S on subject X does not change the state of any other step S′ on the same subject X. The Active/terminal state of each step is determined solely by whether
approve,reject, orwithdrawhas been called on that specificstep_id. -
Invariant 10 — Step store durability. No step record is removed from the store. The total step count is monotonically non-decreasing. A
step_idreturned by a successfulsubmitis durably persisted; astorage-failurerejection guarantees no partial record was written. Terminal steps are retained as audit evidence; deleting a terminal step would destroy the proof that the required approval gate was reached and resolved.
Examples
Happy path — SOX journal entry approval
See Flow section. A complete approval arc is walked there: submission by a controller, review by the finance director, approval, and a later audit query recovering the full decision record. The rejection variant and the withdrawal variant are also walked in the Flow section.
Rejection path — approver not the named actor
A workflow automation engine has a bug and routes the approve call for step-001 to a different finance director: approve("step-001", decided_by: "finance_director_patel", reason: "Looks fine") → rejected(unauthorized). The step remains Pending. The named approver on step-001 is finance_director_chen; finance_director_patel is not authorized to decide on it. The system logs the unauthorized attempt and alerts the process owner.
Rejection path — decision attempted on a terminal step
After step-001 has been Approved, the submitting system retries due to a network glitch: approve("step-001", decided_by: "finance_director_chen", reason: "retry") → rejected(not-pending). The step record is unchanged. The retry system detects the rejection and suppresses further retries.
Rejection path — submit with whitespace-only scope
submit(subject_ref: "po-2026-0099", approver_ref: "procurement_lead", submitter_ref: "buyer_jones", scope: " ") → rejected(invalid-request). Whitespace-only scope is treated as empty. No step is created.
Rejection path — approve with backdated timestamp before submission
approve("step-007", decided_by: "qa_director_kim", decided_at: "2026-01-01T00:00:00Z") where step-007 has submitted_at: "2026-05-01T09:00:00Z" → rejected(invalid-request). The resolved decided_at is 2026-01-01, which is less than submitted_at 2026-05-01; the temporal ordering is violated. The atom rejects regardless of whether decided_at was caller-supplied or would have been wall-clock-defaulted — the enforcement is against the resolved value.
Regulated adversarial scenarios
Regulator audit — SOX §404 control evidence query
A SOX auditor requests evidence that financial journal entries above a materiality threshold were approved by the appropriate controller during Q1 2026. The compliance team queries read({scope: "financial:journal-entry:post", state: Approved, submitted_at: {after: "2026-01-01T00:00:00Z", before: "2026-03-31T23:59:59Z"}}). The result is every Approved step in scope during Q1. Every step carries approver_ref, decided_by, submitted_at, decided_at, and scope — each with at least one non-whitespace character and with timestamps set — immutable by Invariants 1 and 6. The auditor confirms that decided_by matches approver_ref on each step (Invariant 4 makes this structurally enforced, not procedurally promised). The auditor also checks for any steps in Pending that fall within the scope and period, verifying no steps were left unresolved: read({scope: "financial:journal-entry:post", state: Pending, submitted_at: {after: "2026-01-01T00:00:00Z", before: "2026-03-31T23:59:59Z"}}). A non-empty result indicates at least one gate was submitted during Q1 but never decided — a control gap the auditor would surface. The covered entity has documentable, auditable control evidence; no recourse to developer testimony or source code is needed.
Disputed approval — FDA 21 CFR Part 11 electronic signature challenge
An FDA investigator reviewing a pharmaceutical batch release challenges the authenticity of an approval: the actor named in decided_by on step step-batchrel-0412 claims they did not approve batch BR-2026-0412. The investigator queries read({step_id: "step-batchrel-0412"}) and retrieves the step record. The record shows submitted_at: 2026-04-15T14:22:00Z, decided_at: 2026-04-15T16:04:00Z, decided_by: "qp_director_santos", decision_reason: "Batch release authorized — COA reviewed, specification limits met", state: Approved. Invariant 4 guarantees that decided_by matching approver_ref was enforced at the time of the approve call — no other actor could have produced this record. The investigator then invokes Actor Identity to verify the attestation that binds qp_director_santos to the action at decided_at — that is the electronic signature in the Part 11 sense. The Approval Step record is the gate; Actor Identity is the signature. The denied-approval claim cannot be sustained against the structural record without claiming that qp_director_santos’s credentials were compromised — an out-of-band investigation the Part 11 framework also addresses.
Breach or incident forensics — unauthorized approval attempt investigation
During a security incident review, the incident response team needs to determine whether any approval steps were decided by actors other than the named approver during a window of suspected credential compromise (2026-05-01T00:00:00Z through 2026-05-03T23:59:59Z). The team queries read({decided_at: {after: "2026-05-01T00:00:00Z", before: "2026-05-03T23:59:59Z"}, state: Approved}) and read({decided_at: {after: ...}, state: Rejected}) to get all decided steps in the window. Because Invariant 4 is enforced at the action level — any approve or reject where decided_by does not match approver_ref is rejected as unauthorized and produces no record — every step in the decided result set has decided_by = approver_ref by construction. The forensic question is whether the approver_ref actor’s credentials were used legitimately; that question is answered by Actor Identity and the composing credential-compromise investigation, not by this atom. The atom’s records faithfully document every gate and outcome; neither gaps nor fabrications are possible within the atom’s own invariants.
Generation acceptance
Any implementation derived from this atom must produce records and a runtime surface that pass the following checks from the records alone, without recourse to source code, runbooks, or developer narration:
-
Step completeness check. In test and audit environments where the
step_idvalues returned bysubmitcalls are observable, confirm for the set of known-issuedstep_ids thatread({step_id: X})returns each of them across all states. No issuedstep_idmay be absent from the store. In production audit contexts where the auditor does not have the originalsubmitresponses, this exact check is unavailable from the records alone; the equivalent assurance for production audit comes from check 6 (store monotonicity), which detects post-creation deletion without requiring the auditor to enumerate the full set of issued ids. Both checks together constitute the records-alone clearance for Invariant 10 (step store durability). -
Submission attribution check — all states. For every step in the store, in any state: confirm
subject_ref,approver_ref,submitter_ref,scope, andstep_ideach contain at least one non-whitespace character, and confirmsubmitted_atis set (present, not null). This applies equally to Pending, Approved, Rejected, and Withdrawn steps — Invariant 8 covers all states. A step in any state with a blank attribution string or a missingsubmitted_atis a conformance failure under Invariant 8. -
Terminal attribution check — Approved, Rejected, and Withdrawn steps. For every Approved step: confirm
decided_bycontains at least one non-whitespace character, confirmdecided_atis set, and confirmdecided_at ≥ submitted_at(Invariants 6 and 7). For every Rejected step: additionally confirmdecision_reasoncontains at least one non-whitespace character (required for Rejected; optional for Approved). For every Withdrawn step: confirmwithdrawn_bycontains at least one non-whitespace character, confirmwithdrawal_reasoncontains at least one non-whitespace character, confirmwithdrawn_atis set, and confirmwithdrawn_at ≥ submitted_at(Invariants 6 and 7). A terminal step with a blank attribution string, a missing timestamp, an inverted temporal ordering, or a Rejected step without adecision_reasonis a conformance failure under Invariants 6 and 7. The check covers all three terminal states; an implementation that correctly attributes Approved and Rejected steps but leaves Withdrawn steps without required attribution fields fails this check. -
Approver exclusivity check. Submit a step naming approver A. Attempt
approvewithdecided_byset to a different actor B. Confirm the call returnsrejected(unauthorized). Confirm the step remains in Pending state. Then attemptapprovewithdecided_by: A. Confirm the call returnsapprovedand the step transitions to Approved. Invariant 4 guarantees this; the check verifies it. -
Terminal absorption check. Transition a step to each terminal state (Approved, Rejected, Withdrawn). Attempt
approve,reject, andwithdrawon each terminal step. All calls must returnrejected(not-pending). Confirm the step’s fields are unchanged after each attempted transition. Invariant 3 guarantees absorption; this check verifies all three terminal states. -
Store monotonicity check. At time
t1, issueread({})(unfiltered) and record the result set S1. Submit one new step and confirm thesubmitcall returned astep_id. At timet2 > t1, issueread({})again and record result set S2. Confirm every step in S1 appears in S2 bystep_id(no step is removed). For each step present in both S1 and S2, confirm the submission fields (step_id,subject_ref,approver_ref,submitter_ref,scope,submitted_at, andreasonif it was set in S1) are unchanged in S2 — submission fields are immutable per Invariant 1. Decision fields (decided_by,decision_reason,decided_at) and withdrawal fields (withdrawn_by,withdrawal_reason,withdrawn_at) may newly appear on steps transitioned between t1 and t2; their appearance is conformant with the state machine and is not a monotonicity violation. The state of any step in both sets may legitimately have transitioned from Pending to a terminal state; the reverse transition is a conformance failure. The total step count in S2 is ≥ the count in S1.
Edge cases and explicit non-goals
-
Approval Step is not idempotent. Two
submitcalls for the samesubject_ref,approver_ref, andscopecreate two independent steps with distinctstep_ids and independent lifecycles. For at-most-once semantics on submission under retry conditions, compose with Duplicate Prevention. -
Multiple concurrent steps on the same subject. A subject may have multiple simultaneous Pending steps — one per approval gate. Each is independent; deciding on one does not affect the others (Invariant 9). Whether a multi-gate requirement is satisfied — e.g., both the finance director and the legal director must approve — is a composing concern belonging to the Multi-Party Approval composition, not to this atom.
-
Delegation. The atom enforces that
decided_bymatchesapprover_ref(Invariant 4). It provides no mechanism for the named approver to delegate their authority to another actor for a bounded period or scope. Delegation is a composing concern: a delegation pattern would produce a new Approval Step with a differentapprover_refnaming the delegate, or would intercept theapprovecall with a composing authorization check that permits the delegate to act on behalf of the original approver. Either way, the delegation logic is not in this atom. -
Decision reversal. Once a step reaches Approved, Rejected, or Withdrawn, there is no
un-approve,un-reject, orun-withdrawaction. Terminal absorption is invariant (Invariant 3). If a decision was made in error — the wrong approver, an incomplete review, a subsequently discovered misclassification — the correct response is tosubmita new step for the samesubject_refwith an explicit reason documenting the relationship to the original step. The original step’s record remains in its terminal state as evidence of what happened; the new step produces the corrected decision. This produces a more complete audit trail than reversal would; an auditor can see both the original decision and the correction. -
Subject validity.
subject_refis opaque; the atom does not validate it against any external system, document store, or workflow engine. Submitting an approval step for asubject_refthat does not correspond to any real subject creates a step record that names a nonexistent thing. The composing system is responsible for ensuringsubject_refvalues are valid before callingsubmit. -
Decision semantics. The atom records that the named approver approved or rejected the named subject under the named scope. It does not interpret what Approved means for the subject — whether the subject may now proceed, what downstream action is triggered, or what the rejection implies for the subject’s lifecycle. Those are composing-layer and host-system semantics. An Approved step is a fact in the record; what the host system does with that fact is the host system’s concern.
-
Access control. Who may submit steps, who receives notification of pending steps, and who may read them is not defined by this atom. That is the obligation of a composing Permissions pattern. In regulated deployments, submission of approval steps for high-value actions is restricted to authorized process roles; unauthorized submission is a process failure that Permissions governs.
-
Notification. The atom does not notify the named approver that a step is pending. Notification — delivery of the pending-approval signal to the approver’s attention — is a composing concern belonging to the Notification atom and the forthcoming Notification Fanout composition.
-
Approval of a subject that is in a non-approvable state. The atom does not validate that the subject is in a state that makes approval meaningful — for example, approving a document that has already been superseded, or approving a transaction that has already been cancelled.
subject_refis opaque; the composing system is responsible for checking subject state before invokingapproveor before acting on the approval. -
Self-approval and segregation of duties. The atom does not constrain the relationship between
submitter_refandapprover_ref. Asubmitcall where both fields name the same actor is accepted; the resulting step is a self-approval where the same actor will be permitted to callapproveorrejecton the step they themselves submitted. The atom rejects self-approval as a policy concern, not as a structural impossibility. Segregation-of-duties enforcement — the SOX §404 control that requires the submitter and approver of a financial action to be distinct actors, or the FDA Part 11 expectation that an electronic signature attests to a decision the signer did not also originate — belongs to a composing Permissions pattern (which can reject thesubmitorapprovecall when the actor pairing violates a declared segregation policy) and to the Multi-Party Approval composition (which can require a second Approval Step with a differentapprover_ref). The atom records what the calling system submits; the calling system is responsible for the segregation policy. -
Which approvals are required for a given subject. The atom records the approval steps that were submitted; it does not declare which steps should have been submitted for a given subject, scope, or transaction type. The mapping from a transaction or document type to the set of required approval gates — “every journal entry above $10K materiality requires a controller approval and a CFO approval”, “every protocol deviation requires a PI approval” — is the calling system’s business-rule layer. An SOX or GCP auditor asking “show me every journal entry above the materiality threshold that did not receive controller approval” cannot answer that question from this atom’s records alone: the atom can show every submitted Approval Step and its outcome, but it cannot show subjects for which no step was ever submitted. The composing system must supply the transaction set and the required-gate mapping; this atom supplies the gate records that the composition compares against. This is the same boundary as Invariant 5 in Selective Disclosure: completeness with respect to all events the calling system should have produced is an integration obligation, not an atom-level enforcement.
-
Tamper-evidence. The atom guarantees immutability by specification; it does not cryptographically prevent a store administrator from altering step records. For court-admissible evidence of control operation — the bar under SOX §404 and FDA 21 CFR Part 11 — compose with Tamper Evidence, which provides cryptographic sealing of the step records. Tamper-evident approval records are required in several regulated contexts.
-
Non-repudiation. The atom records
decided_byas an opaque reference and enforces that it matchesapprover_ref, but it does not cryptographically bind the deciding actor to the decision in a way that survives disputed-authorship challenges. For non-repudiable approval decisions — the requirement under FDA 21 CFR Part 11 that electronic signatures be uniquely attributable and verifiable — compose with Actor Identity. The Approval Step is the gate record; Actor Identity provides the electronic signature that binds the named actor to the decision. -
Clock semantics.
submitted_at,decided_at, andwithdrawn_atdefault to the receiving node’s wall clock when not supplied.submitted_atmust not be in the future. Backdatedsubmitted_atvalues are accepted.decided_atandwithdrawn_atmust not be in the future and must be ≥submitted_at(enforced at the respective Decision points). Backdateddecided_atandwithdrawn_atvalues are accepted when ≥submitted_at— documenting a decision or withdrawal that was communicated or recognized at an earlier time is valid. Clock skew, timezone normalization, and monotonicity are deployment concerns. -
Concurrency. Two systems concurrently calling
approveorrejecton the samestep_idmust be serialized. The first succeeds; the second receivesnot-pending. Similarly, concurrent calls towithdrawon the samestep_idare serialized. Implementations must serialize state transitions on a givenstep_id. -
Atomicity and crash semantics. Each terminal transition (
approve,reject,withdraw) writes multiple fields simultaneously:state, an attribution field (decided_byorwithdrawn_by), a timestamp (decided_atorwithdrawn_at), and forrejectandwithdrawa reason field. A crash mid-transition that sets some fields without others would violate Invariant 6 (decision attribution is complete for terminal steps). The implementor is responsible for the transactional boundary that makes all fields in a single terminal transition change together. The spec does not define recovery semantics for partial writes; implementations must provide atomic transaction support or a crash-recovery scan that detects and repairs partial transitions on restart.storage-failureis the observable signal of an aborted transition; per the step store durability guarantee, astorage-failureresponse leaves the step in its prior state (Pending) with no partial attribution written. -
Multi-party approval and quorum. When an action requires N approvals from a designated set of approvers — all-of-N, threshold-of-N, one-of-N — each required approval is modeled as a separate Approval Step instance. The quorum (the minimum number of approvals required for a decision to be valid) rule (how many Approved steps constitute sufficient authorization) belongs to the Multi-Party Approval composition (C4). This atom specifies the single gate; the composition specifies the chain.
Composition notes
Approval Step is the approval-gate primitive that Multi-Party Approval and Stateful Workflow Execution compose from:
- Permissions — governs who may submit approval steps, who may read them, and (in conjunction with Actor Identity) who is authorized to hold the
approver_refrole for a given scope. In a conforming SOX deployment, the Permissions atom confirms that the actor callingsubmitholds the required permission to initiate approval gates for the given scope, and that the namedapprover_refholds the permission to approve that scope. - Assignment — Approval Step and Assignment are composing peers, not overlapping concerns. Assignment tracks who owns the work; Approval Step records the gate decision. In multi-actor workflows, an assigned actor may produce the artifact that is then submitted for approval. The two atoms often appear together in the same workflow step.
- Actor Identity — provides the electronic signature that binds the named approver to the decision.
decided_byin the Approval Step record is an opaque reference; Actor Identity is the contract that makes that reference non-repudiable. For FDA 21 CFR Part 11 and SOX §404, the Actor Identity attestation is the signature event; the Approval Step is the gate record the signature attaches to. - Event Log — every
submit,approve,reject, andwithdrawaction is an auditable event. Event Log provides the full state-transition journal; the Approval Step record is the current-state projection. The composing Audit Trail wires Event Log with Actor Identity, Retention Window, and Tamper Evidence into the structure SOX and Part 11 require. - Audit Trail — the canonical regulated-audit stack that provides tamper-evident, attributed, retention-governed records of every approval step lifecycle event.
- Tamper Evidence — seals approval step records against post-hoc modification. Court-admissible approval records require cryptographic integrity guarantees beyond this atom’s spec-level immutability. Required under FDA 21 CFR Part 11 for electronic signature records.
- Duplicate Prevention — for at-most-once semantics on step submission under retry conditions.
- Multi-Party Approval — wires N Approval Step instances under a named quorum rule (all-of-N, M-of-N, one-of-N) into a single auditable chain, layered on Permissions (chain-level authorization), Assignment (in-tray binding per pending step), and Audit Trail (regulated-audit substrate). This atom is the per-gate primitive; the composition is the chain. The composition’s quorum-evaluation rule is the load-bearing wiring decision; the composition’s Generation acceptance names what an auditor can and cannot clear from the chain records alone.
- Workflow / State Machine (forthcoming, atom #9) — the general-purpose state machine engine. Approval Step is a specific kind of state machine with fixed states and approval-specific semantics; Workflow / State Machine is the general case with deployment-declared states and transitions.
- Stateful Workflow Execution (forthcoming, composition C10) — wires Workflow / State Machine + Approval Step + Permissions + Assignment + Event Log + Actor Identity + Audit Trail into multi-actor gated workflows with tamper-evident transition histories.
Standards references
- Sarbanes-Oxley §404 (17 U.S.C. §7262) — internal control over financial reporting. The approval step is the structural form of a financial reporting control: a gate that must be cleared before a material action proceeds, with an auditable record of who cleared it. SOX auditors reviewing control evidence query approval step records directly; Invariants 4, 6, and 8 are the structural guarantees the evidence must carry.
- FDA 21 CFR Part 11 (Electronic Records; Electronic Signatures) — for FDA-regulated contexts (pharmaceutical manufacturing, clinical trials, medical device quality systems): each approval step by a named actor constitutes an electronic signature on the action. Part 11 §11.50 requires that electronic signatures are attributable to one individual and §11.70 requires that they are linked to their respective records to prevent removal, substitution, or falsification. Composition with Actor Identity provides the cryptographic binding; composition with Tamper Evidence provides the linking and non-falsifiability. The Approval Step record is the gate; the composition is the compliant electronic signature system.
- ICH E6(R3) Good Clinical Practice — Guideline — the international standard for clinical trial conduct. Section 4 (Investigator’s Responsibilities) and Section 5 (Sponsor’s Responsibilities) require documented approval steps at multiple points in the trial lifecycle: investigator approval of protocol deviations, IRB approval of informed consent amendments, sponsor approval of site qualification assessments. Each maps to an Approval Step record whose
scopeidentifies the specific GCP obligation. - ISO 9001:2015 §8.5.1 (Control of production and service provision) — requires that production and service provision activities be controlled by documented procedures including approval of documents, products, and services at defined points. Approval steps are the documented approval records §8.5.1 anticipates; the atom’s immutability invariants satisfy the document control requirements.
- ISO 13485:2016 §4.2 (Documentation requirements) — for medical device manufacturers: records of approval activities must be maintained. Approval Step records are the compliant implementation surface.
Status
grounded (passed all required review passes and is stable enough to generate from) — 2026-05-20 — foundation round (Pass 1 + Pass 2 + Pass 3, author-led), Round 2 (AI-conducted, claude-sonnet-4-6), and Round 3 (AI-conducted adversarial, claude-opus-4-7, Torvalds posture) complete. All nine GRID nodes resolved; all concerns conceptually independent; all surfaced adversarial gaps closed in-pattern or named as explicit out-of-scope. First entry in atoms/workflow/.
Lineage notes
Regulated atom. Conventions — Regulated adversarial scenarios and Generation acceptance — inherited from the methodology directly (PRESSURE_TESTING.md), baked in from the first draft. Legal Hold is the reference shape for regulated atoms with two-terminal-to-three-terminal state machine expansion; Provisional Commitment is the reference for multi-terminal-state lifecycle specification. Category atoms/workflow/ is new; this atom opens it.
Pass 1 — Structural completeness (GRID). Five findings, all closed in-pattern.
-
Store instance model absent. Initial draft referenced “the approval store” without defining instance topology. Parallel finding to Legal Hold and every atom composing against a store. Fixed: Store instance model subsection added, mirroring Legal Hold.
step_iduniqueness scoped to instance;subject_refnoted as scoped to the host system; instance selection named as deployment-routing concern. -
rejectaction return token collision. The initial draft returnedrejectedfrom a successfulrejectaction — the same token used by all actions’ error paths. A caller could not distinguish “the step was rejected (by the approver)” from “therejectcall itself was rejected (e.g.,not-known).” Fixed: the successfulrejectreturn is renamedrejected_outcomethroughout. Decision points and Outputs updated to reflect the distinction. -
readordering and filter semantics not defined. The initialreadaction had no stated ordering, no specification of which filter axes are supported, noinvalid-queryconditions, and no statement for an empty result. Fixed:readaction description updated to specifysubmitted_atascending ordering withstep_idas tiebreaker, enumerate valid filter axes, nameinvalid-queryconditions, state that a well-formed query matching no steps returns an empty sequence, and generalize the timestamp-absent-field rule for all filter axes rather than enumerating individual combinations. -
Outputs section under-specified. Initial draft listed only the action return tokens without enumerating which fields are present on which state of step record. Fixed: Outputs now explicitly names core fields present on every step, optional field
reasonas independent of state, and state-specific fields for each terminal state, parallel to Legal Hold’s field listing. -
withdrawaction absent from Inputs and Action signatures. The initial intent named Withdrawn as a state but no action signature forwithdrawappeared in Inputs. Fixed:withdrawadded to Inputs and Actions with full signature, Decision point, and rejection priority.
All nine GRID nodes resolved.
Pass 2 — Conceptual independence (EOS). Clean. Four extraction candidates evaluated; all kept in-pattern.
-
scopeas over-absorption candidate. Couldscopeimply that scope-type classification or policy management belongs in-atom? Evaluated:scopeis an opaque string — the atom does not interpret it, does not import scope semantics, and does not name any scope-management pattern in its specification. Parallel tocase_refin Legal Hold andpolicy_refin Retention Window. The atom records that an approval step covers a named scope; it does not model the scope taxonomy. Clean. -
Delegation as a hidden state. Could the need for delegation mean the atom is missing a Delegated state where a different actor is authorized to decide? Evaluated: delegation is a composing concern. Adding a Delegated state would require the atom to model delegation authority, delegation scope, delegation duration, and possibly nested delegation — concerns that belong to a delegation pattern composing with Permissions, not to this atom. The two-actor model (approver-decides, submitter-withdraws) is the correct EOS design for this atom. Clean.
-
Notification as an absorbed trigger. Could the atom absorb a notification trigger on
submitto alert the named approver? Evaluated: notification delivery belongs to the Notification atom. Absorbing it would make this atom depend on a messaging concern, breaking freestanding status. The atom records that a step is Pending; any notification of the pending step is the composing layer’s responsibility. Clean. -
Quorum logic as a hidden multi-party mechanism. Could tracking multiple steps against the same subject imply the atom should enforce quorum? Evaluated: no. The atom records individual steps; whether a set of approved steps constitutes sufficient authorization for a multi-party requirement is the composing layer’s concern (Multi-Party Approval, C4). The atom does not count approvals or evaluate quorum. Clean.
Pass 3 — Adversarial scrutiny (Linus mode). Twelve findings, all closed in-pattern.
-
Identity model implicit on
approver_refandsubmitter_refauthorization checks. The initial draft nameddecided_byandwithdrawn_byas attribution fields without specifying that these must match the immutableapprover_refandsubmitter_refrespectively, or what happens when they don’t. An implementor could have accepted any well-formed actor reference. Fixed:unauthorizedadded as an explicit rejection reason onapprove,reject, andwithdraw; Invariants 4 and 5 named descriptively and explicitly; Decision points updated with rejection priority placing the identity check after the attribution/temporal checks but beforestorage-failure. -
Rejection priority absent from all action Decision points. Initial Decision points described checks but did not state the order in which multiple failing conditions are evaluated. A caller receiving one rejection reason when multiple conditions fail cannot know which condition to fix first. Fixed: explicit rejection priority added to
approve,reject,withdraw, andreaddecision points, mirroring Legal Hold’s pattern. Priority: malformedstep_id→not-known→not-pending→ attribution/temporal →unauthorized→storage-failure. -
“Supplied” semantics undefined for optional parameters.
submitted_at?,decided_at?, andwithdrawn_at?said “wall clock if not supplied” but did not define what counts as “not supplied.” Fixed: a definition statement added before the action list, mirroring Legal Hold. For optional parameters, “supplied” means provided as a parseable value of the declared type; null, missing, and empty (or whitespace-only) are equivalent to “not supplied” and the default applies. -
Resolved-timestamp enforcement qualified as “if supplied.” The initial draft checked temporal ordering only on caller-supplied timestamp values. A wall-clock-defaulted
decided_aton a clock-skewed node could yield a value <submitted_at, writing a decided step that violates Invariant 7 while the action accepts it. Fixed:approve,reject, andwithdrawaction descriptions and Decision points now require the ordering bound be enforced against the resolved value, regardless of whether it was caller-supplied or wall-clock-defaulted. Invariant 7 statement specifies that constraints apply to persisted values and that enforcement is at the Decision point against the resolved value. -
“Non-empty” and “set” terminology drift. Initial invariants used “non-empty” for both string fields and timestamp fields. Fixed: invariants split by field type — string fields require “at least one non-whitespace character”; timestamp fields require “set.” This matches the Decision point and action signature wording throughout.
-
rejectsuccess token collision with action rejection. Described above as a Pass 1 finding. Surfaced again adversarially: a reader of therejectaction signaturereject(...) → rejected | rejected(...)cannot distinguish the tworejectedtokens from the action description alone. Pass 1 fix confirmed correct; Pass 3 verified the rename torejected_outcomeresolves the ambiguity cleanly. -
Decision reason optionality inconsistency. The initial draft made
reasonoptional on bothapproveandreject. A Rejected step without a stated reason is an auditable record gap — the named approver decided negatively but left no explanation, defeating the corrective action audit trail and the Part 11 requirement for signature meaning. Fixed:reasonis optional onapprove(an approval can be self-evident from the record), required onreject. Invariant 6 reflects the distinction; therejectaction signature and Decision point state it explicitly. -
Intent over-claimed “from the records alone” without naming Tamper Evidence. The initial Intent paragraphs asserted that approval records could be audited from the records alone, without qualifying that cryptographic protection against post-hoc modification requires Tamper Evidence composition. Fixed: Intent paragraph updated to claim immutability by specification and to name Tamper Evidence as the composition that adds cryptographic protection for court-admissible evidence, parallel to Legal Hold’s Intent.
-
readunknown filter key behavior unstated. The initialreaddescription listed supported filter axes but did not say what happened if a caller passed an unknown axis. Fixed:readaction and Decision point now state explicitly that unknown filter keys areinvalid-queryrather than silently ignored, and state the rationale (silent ignore would return a result set inconsistent with the caller’s intent). Mirroring Legal Hold. -
readtimestamp-absent-field rule enumerated for one case only. The initial draft stated the rule fordecided_atfiltering but not forwithdrawn_atfiltering. Fixed:readaction now states the general rule — a time range filter on a timestamp field implicitly excludes states that do not carry that field — rather than enumerating individual combinations, then givesdecided_atandwithdrawn_atas examples. The Decision point mirrors this generalization. -
Concurrency on the same
step_idnot addressed. Two systems concurrently callingapproveon the same Pendingstep_idcould race; without serialization, both might succeed, producing two Approved records for one step. Fixed: Edge case Concurrency added, naming that implementations must serialize state transitions on a givenstep_id. -
Three terminal states not explicitly named as absorbing in a single invariant. Initial draft’s Invariant 3 said “terminal” without enumerating all three terminal states. Fixed: Invariant 3 rewritten to explicitly name all three terminal states (Approved, Rejected, Withdrawn) as absorbing, mirroring Legal Hold’s Invariant 3 structure.
Round 2 — AI-conducted adversarial round (claude-sonnet-4-6, independent re-run). 2026-05-13. Four findings, all closed in-pattern. Round 2 re-ran all three passes with no authoring bias and no recourse to the foundation round’s rationales beyond what the spec itself states.
Pass 1 — GRID structural. All nine GRID nodes verified against the written spec. No new structural gaps found. All cross-references within nodes are intact and consistent. The State section enumerates all four states with correct transition semantics; the Proof (Generation acceptance) section has six numbered checks.
Pass 2 — EOS conceptual independence. All four extraction candidates from the foundation round re-evaluated independently. All conclusions confirmed: delegation, notification, quorum, and scope taxonomy are correctly excluded as composing concerns. No new extraction candidates identified.
Pass 3 — Adversarial scrutiny (Linus mode). Four findings, all closed in-pattern.
-
Generation acceptance check 3 did not cover Withdrawn step attribution. The check was titled “Decision attribution check — Approved and Rejected steps” and verified
decided_by,decided_at, anddecided_at ≥ submitted_atfor Approved and Rejected steps, anddecision_reasonfor Rejected steps. Invariant 6’s third sub-clause — Withdrawn steps must carrywithdrawn_by(at least one non-whitespace character),withdrawal_reason(at least one non-whitespace character), andwithdrawn_at(set) — was tested by no generation acceptance check. An auditor running all six checks against a conforming implementation would verify submission attribution for all states and decision attribution for Approved and Rejected steps, but would leave Withdrawn step attribution unverified. Fixed: check 3 renamed to “Terminal attribution check — Approved, Rejected, and Withdrawn steps” and extended to include the Withdrawn case:withdrawn_bynon-whitespace,withdrawal_reasonnon-whitespace,withdrawn_atset. The closing sentence of the check explicitly names the asymmetric failure mode — an implementation that correctly attributes decided steps but leaves Withdrawn steps unattributed. -
Generation acceptance check 3 verified Invariant 7 for decided steps only. Invariant 7 covers two bounds:
decided_at ≥ submitted_atfor Approved and Rejected steps, andwithdrawn_at ≥ submitted_atfor Withdrawn steps. The prior check 3 verified only the first bound; thewithdrawn_at ≥ submitted_atbound was tested by no check. Fixed: check 3 (now the terminal attribution check) includeswithdrawn_at ≥ submitted_atfor every Withdrawn step. Both sub-clauses of Invariant 7 are now auditor-clearable from the records alone. -
Regulated adversarial scenario 1 asserted a Pending-step check without showing the query. The scenario stated “The auditor also checks for any steps in Pending that fall within the scope and period, verifying no steps were left unresolved” — but did not show the query form that performs this check. An external auditor following the scenario as a procedural script would know a check is expected but not how to execute it. Fixed: the query
read({scope: "financial:journal-entry:post", state: Pending, submitted_at: {after: ..., before: ...}})added inline, with a statement of what a non-empty result signifies (at least one gate submitted but never decided — a control gap the auditor would surface). -
submitDecision point used implicit rather than explicit rejection priority. All three mutating actions —approve,reject,withdraw— end their Decision point entries with an explicit “Rejection priority: X → Y → Z” statement.submitstated preconditions inline and namedstorage-failureas the last-resort rejection, but did not carry the explicit rejection-priority line the other actions do. An implementor reading across all four Decision points would see inconsistent format. Fixed:submitDecision point updated to add “Rejection priority: field-validation (invalid-request) →storage-failure” and to qualify thesubmitted_atfuture-timestamp violation explicitly asinvalid-request, consistent with the other actions.
Round 3 — AI-conducted adversarial round (claude-opus-4-7, Torvalds posture, independent re-run). 2026-05-13. Six findings, all closed in-pattern. Round 3 re-ran all three passes with fresh-reader discipline and no authoring sentiment toward the atom.
Pass 1 — GRID structural. One finding, closed in-pattern.
- TLDR state count off-by-one. The opening paragraph said “Three states — Pending, Approved, Rejected, Withdrawn.” Four states are listed; the sentence claims three. The downstream sentence (“All three terminal states are absorbing”) is correct under the interpretation that “Three” was meant to qualify “terminal,” but as written the count is wrong and any reader new to the atom hits a contradiction in the first paragraph. Round 1 and Round 2 Pass 1 both missed it. Fixed: TLDR rewritten to name the four states explicitly — Pending (the initial state) plus three terminal states (Approved, Rejected, Withdrawn) — and to add the corresponding statement that the submitter is the sole actor authorized to transition Pending to Withdrawn (previously stated only in Invariant 5 and Decision points, not in the TLDR). The “Three terminal states absorbing” claim is preserved unchanged.
Pass 2 — EOS conceptual independence. Clean. The four foundation extraction candidates (delegation, notification, quorum, scope taxonomy) re-evaluated without recourse to prior conclusions; all four conclusions confirmed. Two new candidates considered and excluded:
- Required-gate mapping as an absorbed concern. Could the atom absorb a declaration of which subjects require approval gates? Evaluated: no. A required-gate mapping is a policy concept tied to subject types, materiality thresholds, and regulatory regimes; it has its own state (which subjects-by-attribute must produce gates), its own update lifecycle, and it composes with this atom rather than belonging to it. Pass 3 below names the mapping as an explicit out-of-scope.
- Segregation-of-duties enforcement as an absorbed concern. Could the atom enforce that
submitter_refandapprover_refare distinct? Evaluated: no. SoD is a policy declaration whose granularity (same actor, same role, same department, same legal-entity-of-record) is calling-system-specific and cannot be settled at atom level. It is named below as a composing concern; the atom’s job is to record what was submitted, not to police the actor pairing.
Pass 3 — Adversarial scrutiny (Linus mode). Five findings, all closed in-pattern.
-
Generation acceptance check 1 overclaimed “from records alone.” Check 1 required an auditor to verify that every issued
step_idis present in the store. In production, an auditor reading only the records cannot enumerate all issuedstep_idvalues without the originalsubmitresponses. The check is valid in test and audit-with-runtime environments but not in pure records-alone production audit contexts. Selective Disclosure’s Round 2 Pass 3 closed the parallel finding on its own check 1; Approval Step had not been updated. Fixed: check 1 now scopes itself to test and audit-with-runtime environments wheresubmitreturn values are observable, and directs production audit to check 6 (store monotonicity) for the records-alone equivalent assurance covering Invariant 10. -
readtime-range sub-keysafterandbeforeused in scenarios without formal definition. Regulated adversarial scenarios usedsubmitted_at: {after: ..., before: ...}query forms anddecided_at: {after: ..., before: ...}query forms as ifafterandbeforewere formally defined sub-keys of the time-range filter. They were not defined anywhere in the action description. Selective Disclosure’s Round 2 Pass 3 closed the parallel finding; Approval Step had not been updated. Fixed:readaction description formally specifiesafterandbeforeas optional sub-keys of any time-range filter (submitted_at,decided_at,withdrawn_at), with closed-interval (inclusive) semantics on both bounds, and names that filter keys are flat strings, not dot-notation paths. -
Self-approval and segregation of duties unaddressed. The atom places no constraint on the relationship between
submitter_refandapprover_ref. A self-approval — same actor as both submitter and approver — is structurally accepted. SOX §404 segregation-of-duties controls and Part 11 expectations about signer independence are violated by self-approval in many regulated contexts. The atom never named this as a composing concern, leaving an implementor without guidance on where the enforcement belongs. Fixed: an Edge case added naming self-approval as accepted by the atom and segregation-of-duties enforcement as a composing concern belonging to Permissions (actor-pairing rejection at submit/approve time) and to Multi-Party Approval (requiring a second step with a different approver). The atom’s role — record what the calling system submits — is preserved as the load-bearing boundary. -
Required-approvals-per-subject mapping not named as out-of-scope. The Regulated adversarial scenarios suggested an SOX auditor could query “every journal entry above the materiality threshold during Q1 that was approved” — but never said what the auditor cannot answer from this atom: “every journal entry above the materiality threshold that did not receive controller approval.” The atom records steps that were submitted; subjects that should have had a step but never did are invisible to the atom. The mapping from subject/scope/transaction-type to required-gate set is the calling system’s business rule layer, not the atom’s. Fixed: an Edge case added naming the required-gate mapping as an integration obligation, drawing an explicit parallel to Selective Disclosure’s Invariant 5 (no-disclosure-unrecorded) so the boundary is named once and re-cited. Generation acceptance is unaffected — it never claimed to cover this surface — but the gap is now named where an auditor or implementor will see it.
-
Lineage entries to date did not address whether check 4 and check 5 require runtime exercise versus records-alone reading. Both checks (“Approver exclusivity check” and “Terminal absorption check”) read as records-alone audits but in fact require the auditor to issue
approve/reject/withdrawcalls against the runtime and observe the rejection outcomes. This is consistent with the convention in Generation acceptance across the library — “any code generated from this atom must produce records and a runtime surface that pass the following checks” — but a Linus-posture reader might flag the inconsistency between “from the records alone” and tests that require operating on the runtime. Round 3 confirms the convention is held correctly: the records-alone framing applies to read-driven checks (2, 3, 6) while operational checks (1 in its test-environment form, 4, 5) exercise the runtime surface that the records are produced by. No fix required; the convention is sound. Recorded here so that future Round-N reviewers do not re-surface it as a finding.
Scheduled rescan: 2026-05-20. One refining finding; closed in-pattern.
- Terminal transition crash atomicity unnamed (Pass 3).
approve,reject, andwithdraweach write multiple fields simultaneously (state, attribution field, timestamp, and for reject/withdraw a reason field). A crash mid-transition violates Invariant 6. Clinical Observation and Notification both carry explicit atomicity edge cases; Approval Step had a Concurrency edge case (serialization) but no crash-semantics edge case. Resolved: Edge case Atomicity and crash semantics added, naming the multi-field write requirement and the implementation obligation for transactional atomicity or crash-recovery logic.storage-failureis identified as the signal of an aborted transition that leaves the step in Pending with no partial attribution written.