Approval Workflow Guide: Design a Process You Can Test and Recover

Playcode Team
19 min read
#approval workflow #internal tools #workflow automation

An approval workflow is a record and policy system, not a row of buttons. The useful design starts with one authoritative request, named actors, explicit transitions, a subject version, reviewer assignments, immutable decisions, ordered audit events, and notification attempts that cannot change the business result.

This guide builds a provider-neutral workflow for document and expense-style requests. The same-release TypeScript artifact exercises first-response, all-reviewer, and sequential routing without claiming a native approval provider. It also shows why technical retries, stale human decisions, escalation, delivery failures, and recovery need separate contracts.

Abstract approval sequence with three review gates and a revision loop
Illustrative sequence of a request, ordered review gates, and a revision loop. It is editorial process art, not a product screenshot or evidence that a provider approved or delivered a workflow.

QUICK ANSWER

What is an approval workflow?

An approval workflow is a server-enforced state machine that routes a versioned request to authorized reviewers, records each immutable decision, rejects stale or duplicate changes, and keeps audit and notification history. A reliable design also defines request-changes, rejection, cancellation, reopen, escalation, delivery failure, repair, and recovery before anyone treats an approval button as authoritative.

Write the authority and record model before the screens

Use fictional request data until the access, retention, and notification boundaries have owners. The minimum fixture needs two tenants, an unrelated user, a requester, reviewers, an approver, and an administrator so horizontal and vertical authorization failures are observable.

  • One versioned request subject: Name the request ID, tenant, requester, title or document reference, category, amount or risk attributes, lifecycle state, record version, subject version, timestamps, and retention rule. A resubmission must create a new subject version so an earlier decision cannot appear to approve changed content.
  • An actor and authority matrix: Separate authentication from business authorization. Map requester, reviewer, approver, and administrator actions per tenant and resource, deny by default, and recheck every server request as recommended by the OWASP Authorization Cheat Sheet.
  • A transition and routing table: Write allowed transitions for draft, in review, changes requested, approved, rejected, canceled, and reopened work. For each route, define assigned reviewers, order or quorum, separation of duties, delegation, escalation, stale-write behavior, and whether one response or every response is decisive.
  • Owners for audit, privacy, delivery, and recovery: Name who can inspect and correct records, which request fields are necessary, what routine logs exclude, when requests and audit events are retained or deleted, how authorized exports work, and when to use a compensating repair instead of moving the whole application backward.

Credentials and access

CredentialMinimum accessStorage and rotation
Optional notification adapter credential
A server-side API key, OAuth credential, or SMTP credential from the provider selected after the core workflow passes
Only the sender identity, template, and delivery operation needed for approval notifications; no mailbox, directory, or administrator scope by defaultServer-side secret configuration separated by environment, never request records, browser code, URLs, artifact fixtures, audit reasons, or routine logs Create a replacement, update one environment, send a fictional smoke notification, inspect the provider receipt, then revoke the old credential

Choose the decision pattern from policy, not convenience

Microsoft documents approval types in which one response or everyone must approve, plus a sequential approval example. Those references establish recognizable patterns only. This guide implements its own provider-neutral state machine and does not connect to Power Automate.

ApproachBest forTradeoff
First responseEquivalent reviewers where any one authorized answer is sufficient and the policy explicitly permits the others to close.It reduces waiting, but the first valid response becomes decisive. It is unsafe when two independent controls or separation of duties are required.
All must approveIndependent reviewers whose approvals are all required before the request can reach the approved state.It preserves every required confirmation, but absence and replacement rules must be explicit or one reviewer can hold the request indefinitely.
Sequential reviewPolicies where a later approver should see the result only after an earlier policy, manager, finance, or risk check passes.It expresses ordered authority clearly, but every stage adds latency and needs an absence, delegation, escalation, and cancellation path.

Recommended:Use first response only when reviewers are genuinely interchangeable. Use all-must-approve for independent confirmations and sequential review for ordered authority. The Microsoft first and all-response pattern reference is useful vocabulary, while your own policy must define quorum, conflicts, timeouts, reassignment, and whether a requester may ever approve their own work.

Design and test an approval workflow in eight steps

Define the policy, model versioned records, enforce tenant and role authority, route reviewers, make writes retry-safe, run deterministic tests, publish behind real authentication, and operate with bounded escalation and recovery.

STEP 01

Write the policy as a transition table

Start with states, actors, conditions, and results instead of drawing screens or email templates.

List draft, in review, changes requested, approved, rejected, canceled, and reopened states. For every transition, name the allowed actor, current state, required reason, expected subject version, next state, and notification intent.

Separate approval from authentication, document ownership, delivery, and downstream execution. An approved expense request is a policy decision; it is not proof that payment, procurement, or a provider action succeeded.

Expected result: Every button or API command maps to one allowed transition, and every unlisted state-action pair is denied by default.

Verify it: Review the table with the policy owner and try to express request changes, rejection, cancellation, reopen, delegation, escalation, and an absent reviewer. Add any missing path before implementation.

STEP 02

Model request, assignment, decision, audit, and outbox records

Give the business subject, reviewer work, immutable answer, history, and delivery attempt separate stable identities.

The request holds tenant, requester, subject fields, state, record version, and subject version. An assignment names one reviewer and its order or active status. A decision retains the assignment, reviewer, action, reason, request version, and subject version reviewed.

Append audit events with stable IDs, tenant, request, actor, transition, reason, time, and contiguous order. Store each notification intent in a separate outbox row so delivery can fail or retry without repeating the decision.

approval-records.ts
type ApprovalRequest = {
  id: string
  tenantId: string
  requesterId: string
  state: 'draft' | 'in-review' | 'changes-requested' | 'approved' | 'rejected' | 'canceled'
  version: number
  subjectVersion: number
}

type ApprovalDecision = {
  id: string
  requestId: string
  assignmentId: string
  reviewerId: string
  action: 'approve' | 'request-changes' | 'reject'
  requestVersion: number
  subjectVersion: number
  reason: string
}

type NotificationOutbox = {
  id: string
  requestId: string
  status: 'pending' | 'retry-scheduled' | 'delivered' | 'dead-letter'
  attemptCount: number
}

Expected result: One request can be reconstructed from immutable decisions and ordered audit events while notification attempts remain independently retryable.

Verify it: Create a fictional request and trace its request ID through assignments, a decision, audit order, and outbox rows. Confirm that no record relies on a mutable display label as identity.

STEP 03

Resolve tenant and role authority on the server

Treat a valid session as identity evidence, not permission to read or decide every request.

Resolve the actor, tenant memberships, roles, and active session from server state. On every list, detail, submit, revise, decide, delegate, escalate, cancel, reopen, export, and repair path, recheck tenant and record relationships. Follow OWASP authorization guidance to deny by default and validate every request.

For high-value or sensitive decisions, keep the authorization step distinct from the transaction and require a meaningful reason or confirmation. The OWASP Transaction Authorization Cheat Sheet explains why authorization credentials, transaction data, and the final action need a deliberate binding.

Expected result: Knowing another tenant request ID or holding an unrelated role never reveals, changes, delegates, or approves that request.

Verify it: Repeat the exact known request ID signed out, as an unrelated requester in the same tenant, as an ordinary actor in another tenant, and as each assigned role. Inspect raw server responses, not only hidden controls.

STEP 04

Activate reviewers according to one routing pattern

Turn the policy into explicit active, waiting, approved, rejected, requested-changes, delegated, and closed assignment states.

For first response, activate every equivalent reviewer and close remaining assignments after the first decisive response. For all-must-approve, keep the request in review until every current reviewer approves. For sequential review, activate only the next ordered assignment.

Custom outcomes such as need more information are useful only when they map to a real request-changes state and a new subject version. The Microsoft custom-response approval example demonstrates the pattern, but this workflow does not claim or require a Microsoft connection.

Expected result: Only current assignments can decide, a request-changes answer returns control to the requester, and a resubmission starts a review of the new subject version.

Verify it: Run one fixture for each pattern, then attempt an answer from a waiting sequential reviewer, a closed first-response reviewer, and a reviewer from the prior subject version.

STEP 05

Make retries and concurrent decisions fail safely

Use scoped idempotency for technical attempts and expected versions for human concurrency.

Store an idempotency key with a safe input fingerprint and the resulting stable IDs. Return the prior result for an exact retry and reject changed reuse. RFC 9110 describes idempotent HTTP methods, but an application-specific key and ledger are still a design convention for retrying a non-idempotent mutation such as submit or approve.

Require `expectedVersion` on revisions, decisions, delegation, escalation, cancel, and reopen. In a transactional database, combine unique constraints for stable identities with an intentionally selected transaction isolation level. Re-run the race tests against the database you actually deploy.

Expected result: The same technical attempt returns the same request or decision ID, changed input under the key conflicts, and an old browser cannot overwrite a newer record version.

Verify it: Send two exact requests, one changed-key reuse, and two decisions with the same expected version. Inspect record, decision, audit, and outbox counts after each case.

STEP 06

Run the deterministic artifact before connecting providers

Prove the workflow contract without network traffic, credentials, or provider-specific success states.

Download the same-release reference ZIP, inspect its four-file allowlist, extract it into a fresh directory, run `npm test`, and run `npm run check`. The suite covers wrong role and tenant, exact and changed retries, stale writes, invalid transitions, subject revision, routing, delegation, escalation, notification outage, repair, and recovery.

Compare the ZIP SHA-256 with the evidence note. Re-export the restored fixture and compare canonical bytes and the recovery hash. Corrupt an audit sequence or request snapshot only inside a disposable copy and confirm restore fails closed.

Expected result: All 16 deterministic tests pass with no network requests, and an exact replay preserves stable IDs, audit order, canonical bytes, and the recovery fingerprint.

Verify it: Read the test names and assertions rather than trusting the summary. Confirm the extracted archive has no dependency directory, temporary output, credentials, or files outside the declared allowlist.

STEP 07

Publish behind real authentication and bounded adapters

Deploy the core state machine first, then connect identity and notifications as separately observable boundaries.

Use HTTPS and real server-side session expiry and revocation. Apply schema constraints and transaction behavior in the target database, run migrations against representative data, and repeat role, tenant, stale-write, duplicate, and transition tests through the deployed server.

If email or chat is required, configure the chosen provider account, plan, verified sender or app, minimum server-side credential, rate limits, timeouts, retry policy, and provider receipt mapping. The request and decision must remain durable when delivery is unavailable.

Expected result: The private deployed route enforces the same state and authority contract, while notification status can fail without changing an approved, rejected, or changes-requested decision.

Verify it: Use two ordinary tenant accounts plus each assigned role against direct request URLs and server calls. Then force one safe fictional delivery failure and confirm only its outbox row retries.

STEP 08

Monitor escalation, repair, export, and recovery separately

Operate with stable IDs and bounded corrections instead of treating restore as the answer to every incident.

Monitor transition errors, denied access, stale versions, idempotency conflicts, review age, escalation, outbox attempts, dead letters, audit sequence, and recovery rehearsals using bounded context. Use the OWASP Logging Cheat Sheet to decide which security events matter while excluding secrets, access tokens, and unnecessary private request content.

Escalation should notify or reassign according to policy, never manufacture an approval. Correct a bad audit reason with a compensating event that references the original. Use tenant-scoped export and record repair before a whole-app restore that could move later valid decisions backward.

Expected result: Operators can explain a request by stable IDs, retry a notification, compensate a bounded record, and rehearse recovery without rewriting prior decisions.

Verify it: Choose one fictional approved and one changes-requested request. Reconstruct both timelines, force an escalation, repair one audit reason through a compensating event, export the tenant, restore into an empty target, and reconcile counts and hashes.

Test decisions, not just screens

A green approval screen proves little when the same mutation can be called directly. Exercise the domain and server boundaries with stable fixture IDs, inspect counts after failures, and repeat the suite against the transactional database and authentication layer used in the published environment.

TestScenarioExpected result
happy pathA requester submits subject version 1 to a sequential reviewer and approver; both approve current versions in order.The request becomes approved with two immutable decisions, ordered audit events, and one stable notification intent per observable stage.
invalid inputAn unrelated role, another tenant, a waiting reviewer, or a requester attempts to read or decide a known request ID.The server denies the action without returning private request data, changing state, adding a decision, or creating a delivery attempt.
retryThe same decision request is sent twice, then the same idempotency key is reused with a changed action or reason.The exact retry returns the original stable IDs and counts; changed reuse fails with an idempotency conflict and no additional state.
production smokeOn the private HTTPS route, two tenant accounts and each role exercise direct list, detail, submit, decide, cancel, reopen, export, and notification-status paths.Canonical route, authentication, tenant isolation, transitions, database constraints, audit order, provider boundary, and recovery monitor all match the reviewed release.
invalid inputThe first reviewer advances the request, then a second decision submits the prior expected version or the prior subject version.The stale write fails before a decision, audit event, or notification row is created.
retryNotification delivery fails after a decision, retries before its due time, then succeeds after the due time.The same outbox ID records two attempts while the original request, decision, and audit counts remain unchanged.
happy pathAn active review escalates once because its deadline passed and an administrator receives the notification.The request remains in review, no decision is created, the escalation audit event is stable, and a second escalation fails closed.
production smokeA tenant export restores into an empty target, replays an exact decision, and is re-exported; corrupted audit order and snapshot state are tested separately.Valid replay preserves canonical bytes and hashes; corrupt sequence or state fails; a compensating repair appends history without rewriting the original event.

Diagnose the state boundary before retrying

Approval incidents often look alike in the interface but belong to different records. Start from request, assignment, decision, audit, and outbox IDs, then repair the smallest authoritative boundary.

SymptomLikely causeCheckFix
A reviewer sees the request but a direct approve call is denied.The assignment is waiting, closed, delegated, belongs to another subject version, or the session role and tenant do not match the active assignment.Inspect the server-resolved actor, tenant, request version, subject version, assignment ID, order, and status without logging private subject content.Activate or delegate through the allowed transition. Do not accept a browser-supplied role or force the decision row.
Two clicks appear to create two decisions or notifications.The mutation lacks a unique scoped idempotency ledger, or a notification is coupled to the business write and repeated on response retry.Compare idempotency key, input fingerprint, request version, decision IDs, audit count, and outbox IDs for both attempts.Return the stored result for an exact retry, reject changed reuse, and retry the same outbox row independently.
A reviewer approves content that changed after their page loaded.The decision omitted expected request and subject versions, or a resubmission reused the earlier subject version.Compare the decision requestVersion and subjectVersion with the current request and the audit event that recorded resubmission.Reject the stale decision, increment the subject version on revision, and create current assignments for the new review.
An escalated request becomes approved without an approver decision.A deadline or escalation job incorrectly shares the same transition as an authorized approval.Trace the state-changing audit event and look for a matching immutable decision, active assignment, actor, reason, and expected version.Make escalation non-decisional. It may notify, delegate, or flag the request, but only the authorized decision transition changes approval state.
The request is approved, but the requester received no notification.The notification provider failed after the durable decision, or the outbox row is waiting, not due, or dead-lettered.Inspect decision and audit state first, then the outbox status, attempt count, next retry, bounded error code, and provider receipt.Keep the decision intact and retry only the existing outbox row after its due time. Reconcile an unknown provider outcome before another send.
A restored tenant disagrees with the audit timeline.The bundle was altered, audit sequence is discontinuous, request snapshot does not match the latest event, or recovery omitted idempotency and outbox state.Verify the bundle fingerprint, contiguous event order, latest state per request, stable-ID counters, idempotency ledger, and canonical replay bytes.Reject the restore, preserve the suspect bundle, and use a reviewed earlier bundle or bounded forward repair. Do not rewrite the audit trail to match the snapshot.
An operator wants to restore the whole app to correct one bad reason.Record repair, audit compensation, and whole-app recovery were treated as one tool.Identify the exact bad event, current request state, later valid decisions, expected original fingerprint, and the smallest authorized correction.Append a compensating audit event referencing the original. Reserve whole-app restore for rehearsed recovery scenarios and reconcile post-checkpoint work.

Operate the request, audit, and delivery ledgers independently

Deploy

Deploy behind HTTPS and real server-side authentication. Apply unique request, assignment, decision, audit-sequence, outbox, and idempotency constraints in the target database, choose transaction isolation deliberately, and test real concurrent writes before traffic.

Connect identity, document storage, notifications, payment, procurement, or chat only as named provider adapters with current accounts, plans, permissions, server-side credentials, signatures where applicable, retries, reconciliation, and an explicit degraded state. The reference ZIP proves none of those providers.

Monitor

Monitor denied actions, invalid transitions, stale versions, changed idempotency reuse, review age, escalation count, outbox age and attempts, dead letters, audit gaps, repair events, export size, and recovery rehearsal results by stable IDs and bounded context.

Alert on a decision without an active assignment, state change without an audit event, outbox retry without a durable business record, cross-tenant access, noncontiguous audit order, or restore fingerprint mismatch. Keep subject contents, secrets, and credentials out of routine telemetry.

Recover

Use a compensating event or bounded record repair when the authoritative state is correctable in place. Export the tenant and retain idempotency, assignment, decision, audit, outbox, and counter state before a wider operation.

Restore only into an empty, authorized target after integrity and sequence validation. Rebind sessions rather than restoring them, compare canonical bytes and counts, repeat access tests, reconcile provider receipts, and account for valid post-checkpoint work before cutover.

Keep authority and evidence on the server

The approval result may authorize a sensitive later action, so identity, tenant, subject version, reviewer relationship, and current state must be bound at the server. A polished timeline, email action, or hidden button is not an authorization control.

  • Deny by default and recheck tenant, role, assignment, record relationship, and action on every server request.
  • Prevent self-approval and enforce separation of duties where the policy requires it; administrator access needs a bounded, reviewed purpose.
  • Require expected request and subject versions so a reviewer cannot approve content that changed after they loaded it.
  • Scope every idempotency key to tenant, actor or operation, and input fingerprint; never let changed reuse return the earlier success.
  • Append audit events and compensating repairs instead of mutating prior decisions or history. Protect ordering, access, retention, and export.
  • Store notification and other provider credentials only in server-side secret configuration, apply least privilege, rotate deliberately, and never place them in request data or logs.
  • Minimize personal and document data, bound reasons and logs, define retention and deletion owners, and review regulated workflows with the appropriate legal, security, privacy, and policy owners.
  • Separate record repair, provider reconciliation, authorized export, and whole-app recovery. Rehearse restore and verify integrity before an incident.

Approval workflow questions

What are the main steps in an approval workflow?

The core sequence is draft, submit, assign, review, decide, audit, notify, and operate. A complete workflow also defines request changes, resubmission with a new subject version, rejection, cancel, reopen, delegation, escalation, delivery failure, repair, export, and recovery.

What is the difference between first-response and all-must-approve?

First-response closes the review after the first authorized decisive answer, so it fits interchangeable reviewers only. All-must-approve keeps the request in review until every current required reviewer approves; rejection or request changes may still resolve immediately according to policy.

When should approvals be sequential?

Use sequential review when later authority should act only after an earlier control passes, such as manager review before finance approval. Define absence, delegation, escalation, cancellation, and stale-version behavior for every stage because each ordered gate can delay the request.

Does an idempotency key prevent stale approvals?

No. An idempotency key identifies the same technical attempt and rejects changed reuse. An expected record version and subject version protect concurrent human decisions. A reliable workflow uses both controls and tests them independently.

Should escalation automatically approve a request?

No. Escalation may notify an administrator, flag age, or delegate to another authorized reviewer, but it should not manufacture a policy decision. An approval needs an active assignment, authorized actor, current versions, explicit action, and immutable decision record.

What happens when approval email fails?

Keep the durable request and decision unchanged. Record notification intent in an outbox, classify the failure, retry the same row after its due time, and reconcile unknown provider outcomes. Email delivery must not become the source of truth for approval state.

How do you recover an approval workflow safely?

Prefer compensating events or bounded record repair for local errors. For wider recovery, export tenant-scoped records including idempotency and outbox state, verify integrity and audit sequence, restore into an empty authorized target, rebind sessions, compare canonical results, and reconcile post-checkpoint work.

Does the downloadable artifact connect to Power Automate or another provider?

No. It is a deterministic TypeScript state-machine reference with fictional data and no network calls. Microsoft documentation is cited only for recognizable response and sequential patterns. Native integration, real authentication, database transactions, provider delivery, and production recovery remain separate work.

Build the bounded workflow

Turn your reviewed policy into an internal tool

Describe the request record, roles, transitions, routing pattern, evidence, and failure states. Keep real provider credentials and regulated data out until the deterministic workflow and access tests pass.

Explore internal tools

Playcode can help build a custom internal web app. This guide does not claim a native approval engine, prebuilt provider connector, compliance status, or production-ready policy.

Have thoughts on this post?

We'd love to hear from you! Chat with us or send us an email.