Workflow examples

Three workflows that show the patterns you will reach for most: a multi-step pipeline, a run that pauses for a human, and one that runs on a schedule. Each is a full workflows/<name>.workflow.ts file. Drop it in a workspace and trigger it as shown in Getting started.

1. An onboarding pipeline

A linear pipeline: validate the signup, draft a welcome with a single model call, then persist the result. Each step is a durable checkpoint, so a crash after the draft never re-bills the model call before it.

workflows/onboarding.workflow.ts
import { z, stackbone } from '@stackbone/sdk';

export const inputSchema = z.object({
  userId: z.string(),
  email: z.email(),
  plan: z.enum(['free', 'pro', 'scale']),
});
export const outputSchema = z.object({
  userId: z.string(),
  subject: z.string(),
  body: z.string(),
});

export async function onboardingWorkflow(input: z.infer<typeof inputSchema>) {
  'use workflow';

  const signup = await validateSignup(input);
  const copy = await draftWelcome(signup.plan);
  return await persistWelcome(signup.userId, copy);
}

async function validateSignup(input: z.infer<typeof inputSchema>) {
  'use step';
  if (!input.email.includes('@')) throw new Error(`Invalid email: ${input.email}`);
  return { userId: input.userId, plan: input.plan };
}

async function draftWelcome(plan: string) {
  'use step';
  const result = await stackbone.ai.chat.completions.create({
    model: 'openai/gpt-4o-mini',
    messages: [
      { role: 'system', content: 'Write a one-line welcome email subject and body.' },
      { role: 'user', content: `The user joined the ${plan} plan.` },
    ],
  });
  if (result.error) throw new Error(result.error.code);
  const text = result.data.choices[0]?.message.content ?? '';
  return { subject: `Welcome to the ${plan} plan`, body: text };
}

async function persistWelcome(userId: string, copy: { subject: string; body: string }) {
  'use step'; // idempotent on userId
  return { userId, ...copy };
}

2. A refund gated by a human approval

The run pauses on requestApproval until a person decides, and the side effect is gated behind a fresh approved decision. If nobody decides within the timeout, the fallback wins. requestApproval is a workflow primitive: call it from the body, never from a step.

workflows/refund.workflow.ts
import { z } from '@stackbone/sdk';
import { requestApproval } from '@stackbone/sdk/workflow';

export const inputSchema = z.object({
  orderId: z.string(),
  amount: z.number().positive(),
});
export const outputSchema = z.object({
  orderId: z.string(),
  refunded: z.boolean(),
  decision: z.string(),
});

export async function refundWorkflow(input: z.infer<typeof inputSchema>) {
  'use workflow';

  const decision = await requestApproval({
    token: `refund-${input.orderId}`,
    topic: 'refund',
    payload: { orderId: input.orderId, amount: input.amount },
    title: 'Approve refund',
    timeout: '24h',
    fallback: 'reject',
  });

  if (decision.status !== 'approved') {
    return { orderId: input.orderId, refunded: false, decision: decision.status };
  }

  await performRefund(input.orderId, input.amount);
  return { orderId: input.orderId, refunded: true, decision: decision.status };
}

async function performRefund(orderId: string, amount: number) {
  'use step'; // the non-idempotent side effect, gated behind the approval
  return { refundId: `rf_${orderId}_${amount}` };
}

A reviewer decides from the dashboard or with stackbone hitl approve <id> --yes, which wakes the parked run. The full approval surface is on the human-in-the-loop page.

3. A scheduled digest with a long wait

This workflow runs every morning from its declarative schedules export. It also shows sleep, which parks the run without holding a process open. The run is unscheduled until the timer fires, then resumes from where it slept.

workflows/daily-digest.workflow.ts
import { z, stackbone } from '@stackbone/sdk';
import { sleep } from '@stackbone/sdk/workflow';

export const schedules = [{ cron: '0 8 * * *', input: { scope: 'daily' } }];

export const inputSchema = z.object({ scope: z.enum(['daily', 'weekly']) });
export const outputSchema = z.object({ sent: z.number() });

export async function dailyDigestWorkflow(input: z.infer<typeof inputSchema>) {
  'use workflow';

  const recipients = await collectRecipients(input.scope);

  let sent = 0;
  for (const to of recipients) {
    await deliver(to, input.scope);
    sent += 1;
    await sleep('5s'); // gentle pacing between sends, durable across a restart
  }

  return { sent };
}

async function collectRecipients(scope: string) {
  'use step';
  return scope === 'weekly' ? ['team@acme.com'] : ['sam@acme.com', 'lee@acme.com'];
}

async function deliver(to: string, scope: string) {
  'use step'; // idempotent on (to, scope, day)
  return { to, scope, 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