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.

workflows/triage-lead.workflow.ts
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.

workflows/draft-and-send.workflow.ts
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

The platform for agent developers.Build, ship and observe agentsthat survive prod.

© 2026 · STACKBONEBUILT WITH ❤️ FROM CANADA AND SPAIN