Getting started with workflows
This page takes you from an empty workspace to a workflow you can trigger and watch run. You develop it with
stackbone dev, so everything here happens on your machine. The same files run unchanged inside a packaged agent. If the CLI is new to you, set up Node 24 and Docker with the CLI getting started guide first.
A workflow lives inside a workspace alongside your agents.
The steps below assume you already have one (stackbone init my-workspace).
1. Scaffold a workflow
From inside your workspace:
stackbone add workflow qualify-leadThis writes workflows/qualify-lead.workflow.ts. It needs no login and touches no
cloud: the workflow joins the workspace stackbone init already linked. It only
writes new files, and a name collision fails with an error (re-run with --force
to overwrite).
The file follows the naming convention the runtime scans for: the workflow name is
the file basename without .workflow.ts, and the exported function is the camelCase
name plus Workflow. So qualify-lead.workflow.ts exports qualifyLeadWorkflow.
2. Write the pipeline
A workflow is a plain async function marked 'use workflow'. Every side effect goes
in a sub-function marked 'use step', which the runtime runs once, persists, and
retries on failure. Declare the contract with sibling inputSchema and outputSchema
exports:
import { z } from '@stackbone/sdk';
export const inputSchema = z.object({
email: z.email(),
company: z.string(),
});
export const outputSchema = z.object({
email: z.string(),
score: z.number(),
qualified: z.boolean(),
});
export async function qualifyLeadWorkflow(input: z.infer<typeof inputSchema>) {
'use workflow';
const enriched = await enrich(input.company); // step 1: fetch firmographics
const score = await scoreLead(enriched); // step 2: deterministic scoring
return { email: input.email, score, qualified: score >= 60 };
}
async function enrich(company: string) {
'use step';
// Replace with a real lookup. A step's result is persisted and replayed on resume.
return { company, employees: 250, industry: 'software' };
}
async function scoreLead(enriched: { employees: number }) {
'use step';
return Math.min(100, Math.round(enriched.employees / 5));
}Keep the workflow body deterministic: no clocks, no randomness, no direct I/O. Put
all of that inside a step. That includes every stackbone.* call: the body runs in
an isolated sandbox, and a stackbone.* reference outside a step fails every
workflow in the workspace at run time. See the
overview for why.
3. Triggering a run
A workflow starts as its own run, with its own run id and step log. The two paths you will use most while developing:
From the CLI: start a run by name with a JSON input:
stackbone workflows start qualify-lead \
--input '{"email":"sam@acme.com","company":"Acme"}'List what the installation exposes, or inspect a workflow's contract, with:
stackbone workflows list
stackbone workflows schema qualify-leadOver HTTP: the emulator serves a generic trigger route per workflow. The body is
the workflow input; the emulator validates it against inputSchema before anything
runs:
curl -X POST http://127.0.0.1:4242/api/workflows/qualify-lead/start \
-H 'content-type: application/json' \
-d '{"email":"sam@acme.com","company":"Acme"}'
# → { "workflowName": "qualify-lead", "status": "started",
# "runId": "...", "worldRunId": "...",
# "trigger": "POST /api/workflows/qualify-lead/start" }The emulator rejects a bad payload with field-level issues and creates no run.
4. Watch the run
A started workflow is an ordinary run. Observe it, retry a failed step, or read its logs with:
stackbone runs list
stackbone runs get <runId>
stackbone logs tail --run <runId>Kill stackbone dev mid-run and start it again: the run resumes from the last
completed step instead of replaying the whole pipeline.
5. Run on a schedule
To run the workflow on a cadence, export a schedules array next to it. The runtime
reconciles it on every boot, so there is no imperative call to make:
export const schedules = [{ cron: '0 3 * * *', input: { email: '', company: '' } }];The cron value is a five-field pattern (minute hour day month weekday) or a named
macro (@hourly, @daily, @weekly, @monthly, @yearly), read in UTC. A
pattern the box cannot parse never arms, and its row on the Recurring jobs screen
reads not armed.
To add or drop a schedule at run time instead, call
stackbone.workflows.schedule / .unschedule / .listSchedules from inside a
workflow. Either way, Recurring jobs in Studio shows what is armed right now,
when it fires next, and how the last run went. See
Recurring jobs.
Where to go next
- Examples: an onboarding pipeline, a refund with a human approval, and a scheduled digest.
- Serial execution: queue overlapping triggers instead of running them side by side.
- What is a workflow: the directives, durability, and typed-contract model in full.
- Workflow + Agents: delegate a step to an agent.
- Human-in-the-loop: pause a run for a person to approve.