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.
The whole workflow is a single workflows/onboarding.workflow.ts file. Drop it
into a workspace and trigger it the way
Getting started with workflows
shows.
import { z } from 'zod';
import { stackbone } from '@stackbone/sdk';
import { FatalError } from '@stackbone/sdk/workflow';
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';
// A bad address never becomes good on a retry, so fail the run at once.
if (!input.email.includes('@')) throw new FatalError(`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'; // replace with a real write, keyed on userId so a replay overwrites
return { userId, ...copy };
}Which error to throw
The runtime retries a failed step, so pick the throw that matches the cause:
- Throw a plain
Errorwhen a retry can fix it. The model call indraftWelcomeis the case: a rate limit or a timeout clears on its own. - Throw
FatalErrorwhen nothing can fix it. An invalid email address stays invalid, so a retry burns the step's retry budget and the run still fails.FatalErrorskips the budget and fails the run at once.
What's next
- A refund gated by a human approval: park the run until a person decides.
- A scheduled digest with a long wait:
start a run from a cron schedule and pace it with
sleep. - What is a workflow: the model behind this example.