--- title: 'Draft with an agent, then gate the send on a human' description: 'A workflow that has an agent write an email, parks for a person to approve it, and sends only after the approval lands.' position: 3 --- # Draft with an agent, then gate the send on a human > This flow uses all three building blocks at once. A workflow asks an agent to > draft a reply, pauses for a person to approve it, and sends only once someone > approves. The send lives in its own step behind the approval, so a rejected > run never reaches it. The whole flow is a single `workflows/draft-and-send.workflow.ts` file. The agent's reply arrives as plain text, the same as in [the triage example](/docs/examples/workflow-agents/delegate-to-agent), so `draftReply` parses and validates it before the workflow uses it. ```ts // workflows/draft-and-send.workflow.ts import { z } from '@stackbone/sdk'; import { callDeepAgent, requestApproval } from '@stackbone/sdk/workflow'; export const inputSchema = z.object({ to: z.email(), topic: z.string(), }); export const outputSchema = z.object({ to: z.string(), sent: z.boolean(), draft: z.string(), }); const draftSchema = z.object({ subject: z.string(), body: z.string() }); export async function draftAndSendWorkflow(input: z.infer) { 'use workflow'; const draft = await draftReply(input.topic); // agent writes the draft const decision = await requestApproval({ // No `token`: the SDK assigns a resume key unique to this run. topic: 'outbound-email', payload: { to: input.to, draft }, title: 'Approve this email before it goes out', timeout: '24h', fallback: 'reject', }); if (decision.status !== 'approved') { return { to: input.to, sent: false, draft: draft.body }; } await send(input.to, draft); // gated side effect return { to: input.to, sent: true, draft: draft.body }; } async function draftReply(topic: string) { 'use step'; const { text } = await callDeepAgent( 'support', `Draft a short, friendly email about: ${topic}. ` + 'Reply with JSON only: {"subject": string, "body": string}.', ); return draftSchema.parse(JSON.parse(text)); } async function send(to: string, draft: { subject: string; body: string }) { 'use step'; // the side effect, gated behind the approval. Keep it idempotent return { to, subject: draft.subject, delivered: true }; } ``` Two details decide whether this holds up in production. - Leave `token` unset. The SDK then assigns a resume key unique to the run, so two drafts for the same recipient and topic cannot collide. A collision resolves to the `fallback` instead of waiting for a person. See [when to pass your own token](/docs/sdk/humans/approval#when-to-pass-your-own-token). - Keep `send` idempotent. The runtime re-runs a step that crashes part-way through, so give the outbound message a stable id and let a repeat call recognise a message it already delivered. ## What's next - **[A workflow that delegates a turn to an agent](/docs/examples/workflow-agents/delegate-to-agent)**: the agent half of this flow on its own. - **[An agent that starts a workflow](/docs/examples/workflow-agents/agent-starts-workflow)**: the same two pieces, with the agent in charge. - **[What is a workflow agent](/docs/sdk/workflow-agents/overview)**: the model behind this flow. - **[Human-in-the-loop](/docs/sdk/humans/approval)**: the full approval surface used here.