Approvals & Email Flows
Plan approval, step-level approvals, and the bot-prefetch guard.
Plan Approval
After a workflow is created, the team owner receives a plan approval email with a step-by-step breakdown and an "Approve & Activate" button.
The button links to:
GET /api/workflows/[id]/approve?token=<24h-JWT>
When the user clicks it:
- Token is verified via
verifyToken()fromlib/ai/workflows/tokens.ts - Workflow status set to
"active" - A new
conversationis created (withniche: "workflow") - A new
workflowRunis created withstatus: "running" - Step 0 is enqueued to QStash with a 2-second delay (allows DB commit to propagate)
- User is redirected to
/dashboard/workflows/[id]
Step-Level Approval
When a workflow step's tool has requiresApproval: true (e.g., format_post):
- The step agent runs, produces the tool output
saveAgentResponse()returnstoolNeedsApproval: true- The step executor sets
workflowRun.status = "awaiting_approval" sendStepApprovalEmail()is called with the tool's output as a preview- The user receives an email with Approve / Request Changes buttons
On approve: GET /api/workflows/resume?token=<2h-JWT>&action=approve
- Token verified
- Run status set to
"approved" - QStash re-enqueues the same step index with a
userResponse: "Approved. Please continue."message prepended to the conversation
On reject: GET /api/workflows/resume?token=<2h-JWT>&action=reject¬e=<feedback>
- Token verified
- Run status set to
"approved"(re-runs the step) - QStash re-enqueues with
userResponse: "Rejected. Feedback: <note>. Please revise."prepended
JWT Token Types
Both token types are signed with WORKFLOW_TOKEN_SECRET (HS256 via jose):
// lib/ai/workflows/tokens.ts
type TokenPayload =
| { type: "plan_approval"; workflowId: string }
| { type: "step_approval"; workflowId: string; runId: number; conversationId: string; stepIndex: number }| Token | Expiry | Used By |
|---|---|---|
plan_approval | 24 hours | Plan approval email link |
step_approval | 2 hours | Step approve/reject email links |
Bot-Prefetch Guard
Email clients (Outlook, Gmail, Apple Mail) automatically send a GET request to every link in an email to generate link previews. Without protection, this would trigger approvals before the user opens the email.
GET /api/workflows/resume checks the incoming User-Agent header against a list of known bot patterns. If matched, it returns 200 OK with no side effects. Human browser UAs proceed with the full approval logic.
Never disable the bot-prefetch guard. A single prefetch from an email client can approve or reject a workflow step instantly, before the user even opens the email.