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.tsfile. 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.
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.
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.
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
- What is a workflow: the model behind these examples.
- Workflow + Agents: let a step hand work to a conversational agent.
- Human-in-the-loop: the full
requestApprovalsurface and the deciding commands.