Workflow agent examples
Three flows that combine a durable workflow with an agent. Each shows a different way the two pieces fit together. They build on the agent and workflow examples, so the building blocks should look familiar.
callDeepAgent always resolves with plain text, an agent turn has no typed
output of its own. When a step needs structured data back, ask for it in the
message and validate the reply, as the first example below does.
1. A workflow that delegates a turn to an agent
A triage workflow validates the incoming lead deterministically, then hands
the judgment to a lead-qualifier agent and parses a typed verdict out of
its reply. The workflow stays auditable. The agent does the open-ended
scoring.
import { z, stackbone } from '@stackbone/sdk';
import { callDeepAgent } from '@stackbone/sdk/workflow';
export const inputSchema = z.object({
email: z.email(),
company: z.string(),
note: z.string(),
});
export const outputSchema = z.object({
email: z.string(),
score: z.number(),
rationale: z.string(),
});
const verdictSchema = z.object({
score: z.number().min(0).max(100),
rationale: z.string(),
});
export async function triageLeadWorkflow(input: z.infer<typeof inputSchema>) {
'use workflow';
const clean = await validate(input); // deterministic gate
const verdict = await scoreWithAgent(clean.company, clean.note); // agent judgment
return { email: clean.email, score: verdict.score, rationale: verdict.rationale };
}
async function validate(input: z.infer<typeof inputSchema>) {
'use step';
if (!input.company.trim()) throw new Error('Missing company');
return input;
}
async function scoreWithAgent(company: string, note: string) {
'use step';
const { text } = await callDeepAgent(
'lead-qualifier',
`Company: ${company}\nNote: ${note}\n` +
'Score this lead from 0 to 100 and explain why. ' +
'Reply with JSON only: {"score": number, "rationale": string}.',
);
// A malformed reply throws here, which fails the step and applies the
// run's retry policy: a retry re-runs the whole turn.
return verdictSchema.parse(JSON.parse(text));
}The lead-qualifier agent's system prompt should tell it to always answer in
that exact JSON shape, so the step can trust the parse.
2. An agent that starts a workflow
Here the agent owns the conversation and offloads slow work. A tool kicks
off an enrich-profile workflow and returns right away, so the user is not
left waiting while the background job runs.
// deep-agents/concierge/index.ts (excerpt)
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { stackbone } from '@stackbone/sdk';
const enrichProfile = tool(
async ({ userId }: { userId: string }) => {
const { runId } = await stackbone.workflows.start('enrich-profile', { userId });
return JSON.stringify({ started: true, runId });
},
{
name: 'enrich_profile',
description: 'Kick off background enrichment for a user profile.',
schema: z.object({ userId: z.string() }),
},
);If the agent needs the workflow's result before it replies, swap start for
startAndWait, which suspends durably until the workflow finishes and
returns its validated output:
const profile = await stackbone.workflows.startAndWait<{ tier: string }>('enrich-profile', {
userId,
});
return JSON.stringify({ tier: profile.tier });3. Draft with an agent, then gate the send on a human
The richest pattern combines all three building blocks. A workflow asks an agent to draft a reply, pauses for a person to approve it, and only sends once approved. The draft is open-ended, the approval is durable, and the send is gated.
import { z, stackbone } 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({
token: `send-${input.to}-${input.topic}`,
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 non-idempotent side effect, gated behind the approval
return { to, subject: draft.subject, delivered: true };
}Where to go next
- What is a workflow agent: the model behind these flows.
- Agent examples and Workflow examples: the building blocks on their own.
- Human-in-the-loop: the full approval surface used in the third example.