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, so
draftReply parses and validates it before the workflow uses it.
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<typeof inputSchema>) {
'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
tokenunset. 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 thefallbackinstead of waiting for a person. See when to pass your own token. - Keep
sendidempotent. 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: the agent half of this flow on its own.
- An agent that starts a workflow: the same two pieces, with the agent in charge.
- What is a workflow agent: the model behind this flow.
- Human-in-the-loop: the full approval surface used here.