Run an agent whose side-effecting actions (refunds, emails, deletes) wait up to 7 days for a human decision without holding a process, survive crashes and redeploys, resume within 2 seconds of approval, and produce 0 duplicate side effects in a 100-run chaos test.
Recommended stack
- Workflow SDK 5 (npm workflow) - Durable orchestration with "use workflow" / "use step", hooks, and sleep. createHook/defineHook pause a run until resumeHook is called, with steps retried and replayed after crashes.
- @workflow/world-postgres 5 (or the managed world on Vercel) - Run, step, and hook persistence when self-hosting. Stores state in your own Postgres with an idempotent bootstrap command and a graphile-worker queue.
- AI SDK 7 - Model calls and tool definitions. Typed tools plus toolApproval surface proposed side effects as approval requests instead of executing them.
- Next.js 16 - Approvals inbox UI and the resume API route. Server actions and route handlers keep the approver check and resumeHook call on the server.
Build steps
- Classify every tool as read-only (runs freely) or side-effecting (needs approval). Define the approval payload with zod: action, arguments, reason, requested_by, approver_id.
- Write the workflow with "use workflow": model step ("use step") proposes the next action; for a side-effecting action, create the hook with createHook({ token, metadata: { approverId } }) and validate the resume payload with the zod schema (or defineHook({ schema }) if you keep approver_id in your own table), and send the approval link in a notification step.
- Create the hook promise once and race it against
sleep("7d"); on timeout record "expired", notify the requester, and take no action. - Put each side effect in its own "use step" and pass getStepMetadata().stepId (stable across retries) as the idempotency key to the downstream API (payments, email), so retries cannot double-fire.
- Add POST /api/approvals/[token]: load the hook, confirm the signed-in user equals approver_id, then call resumeHook(token, { approved, note }); return 404 for unknown tokens and 403 for the wrong user.
- Self-hosting: install @workflow/world-postgres, set WORKFLOW_TARGET_WORLD and WORKFLOW_POSTGRES_URL, run
npx --package=@workflow/world-postgres bootstrapafter each deploy, and block public access to /.well-known/workflow/v1/flow. - Build an approvals inbox that lists pending hooks with the proposed action, its arguments as a diff, the agent's reason, and approve/deny with a note.
- Write a chaos script: start 100 runs, kill the server mid-step, redeploy during waits, approve at random delays, and assert each approved action ran exactly once.
Watch out for
- Treating the hook token as authentication: it identifies the hook, not the sender. Always check the signed-in approver before resumeHook.
- Non-deterministic code in the workflow body (Date.now, Math.random, direct fetch) breaks replay. Keep all I/O inside steps.
- Creating a new hook promise inside each loop iteration misses events that arrive between races. Create it once and reuse it.
- Side-effect steps without idempotency keys duplicate refunds or emails when a step retries.
Definition of done
- Start a run, restart the server, approve an hour later: the action executes once and the run completes.
- Opening the approval link as a different signed-in user returns 403 and the run stays paused.
- A run left unapproved expires at the timeout with no side effect and the requester is notified.
- The chaos script reports 0 duplicate side effects across 100 runs.