← library
promptexample · tested🤖 AI agentsv1 · published 2026-10-15 · updated 2026-11-03

Agent with durable human approval

An agent that pauses days for a human yes/no, survives deploys, and runs each action once, with Workflow SDK.

By Uplift Duo

Run it as a prompt

Paste this into any AI agent, or fetch it: curl -s https://uplift.page/api/v1/prompts/agent-human-approval-workflow/raw

prompt.md
# Agent with durable human approval

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

1. 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.
2. 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.
3. Create the hook promise once and race it against `sleep("7d")`; on timeout record "expired", notify the requester, and take no action.
4. 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.
5. 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.
6. Self-hosting: install @workflow/world-postgres, set WORKFLOW_TARGET_WORLD and WORKFLOW_POSTGRES_URL, run `npx --package=@workflow/world-postgres bootstrap` after each deploy, and block public access to /.well-known/workflow/v1/flow.
7. 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.
8. 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.

The full prompt

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

  1. 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.
  2. 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.
  3. Create the hook promise once and race it against sleep("7d"); on timeout record "expired", notify the requester, and take no action.
  4. 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.
  5. 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.
  6. Self-hosting: install @workflow/world-postgres, set WORKFLOW_TARGET_WORLD and WORKFLOW_POSTGRES_URL, run npx --package=@workflow/world-postgres bootstrap after each deploy, and block public access to /.well-known/workflow/v1/flow.
  7. 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.
  8. 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.

Served from the uplift.page library and refreshed within 5 minutes of every update.