human approval

why

an approval is a business decision that outlasts any browser session: whoever resumes the process months later must see who decided, when, and on exactly what. a transient button click evaporates, cannot be audited, and can be replayed or forged without authority checks. persisting the request and the decision as durable workflow state makes the approval itself the auditable fact the process advances from.

rule

a human approval is a durable workflow state transition, not a transient button click. record what decision is requested, wait for a typed response, validate authority and freshness, then record approval or rejection before resuming.

required flow

  1. persist an approval request with its workflow instance, subject, requested action, and expiry.
  2. render that record to the user through the Agent or ui.
  3. accept a typed decision at an authorized boundary.
  4. persist the decision exactly once under a stable approval identity.
  5. signal the workflow; it reloads and validates the persisted decision before advancing.

decision process

approval is an asynchronous command, not proof that work may proceed. the ui may request a decision; it may not manufacture one. the server owns whether the actor is authorized, whether the request is current, and whether the decision is already consumed.

riskdurable response
two tabs approve at once.one stable approval identity and exactly-once decision write.
an old page submits after the request changed.expiry/version validation before recording the decision.
an unauthorized actor posts the same shape.Authorization at the decision boundary, not in UI visibility alone.
the workflow wakes before persistence commits.reload the stored decision before any transition.

good and bad

bad

approve = async (requestId) => {
await this.broadcast({ approved: true });
await resumeWorkflow(requestId); // no durable decision record
};

a client claim advances the process with no authority, expiry, or exactly-once check — two tabs or an unauthorized actor create duplicate transitions, a stale page can resurrect a superseded decision, and no auditable record exists because none was ever written.

good

approve = async (requestId, actor) => {
assertAuthorized(actor, requestId);
const decision = await recordDecisionOnce(requestId, actor, "approved");
await signalWorkflow(decision.workflowInstanceId);
};

the decision persists exactly once under a stable approval identity after authority and freshness checks; the workflow reloads that record before advancing. every step is explainable from durable facts — who decided, when, on which subject — which audits, disputes, and later resumes rely on. duplicate submissions cannot manufacture a second transition: the identity allows only one decision write.

verification reasoning

exercise approval, rejection, duplicate submission, expired submission, and unauthorized submission. every result must leave a queryable approval record; only a current, authorized, persisted approval advances the process.

do not

  • resume because a client says it approved without a durable approval record.
  • treat a missing, expired, or unauthorized response as approval.
  • hold a request open or poll a client while the workflow could wait for a durable event.
  • let the UI choose a workflow transition the server has not authorized.

search pages

go to any page