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
- persist an approval request with its workflow instance, subject, requested action, and expiry.
- render that record to the user through the Agent or ui.
- accept a typed decision at an authorized boundary.
- persist the decision exactly once under a stable approval identity.
- 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.
| risk | durable 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.