--- title: 'Getting started' description: 'Scaffold a durable workflow, trigger a run from the CLI or over HTTP, and watch it complete.' position: 2 --- # 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](/docs/cli/guides/getting-started) > guide first. A workflow lives inside a **workspace** alongside your [agents](/docs/sdk/agents/overview). The steps below assume you already have one (`stackbone init my-workspace`). ## 1. Scaffold a workflow From inside your workspace: ```sh stackbone add workflow qualify-lead ``` This 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: ```ts // workflows/qualify-lead.workflow.ts 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) { '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](/docs/sdk/workflows/overview#the-execution-model) 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: ```sh stackbone workflows start qualify-lead \ --input '{"email":"sam@acme.com","company":"Acme"}' ``` List what the installation exposes, or inspect a workflow's contract, with: ```sh stackbone workflows list stackbone workflows schema qualify-lead ``` **Over 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: ```sh 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: ```sh stackbone runs list stackbone runs get stackbone logs tail --run ``` 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: ```ts 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](/docs/home/features/recurring-jobs). ## Where to go next - **[Examples](/docs/examples/workflows/onboarding-pipeline)**: an onboarding pipeline, a refund with a human approval, and a scheduled digest. - **[Serial execution](/docs/sdk/workflows/serial-execution)**: queue overlapping triggers instead of running them side by side. - **[What is a workflow](/docs/sdk/workflows/overview)**: the directives, durability, and typed-contract model in full. - **[Workflow + Agents](/docs/sdk/workflow-agents/getting-started)**: delegate a step to an agent. - **[Human-in-the-loop](/docs/sdk/humans/approval)**: pause a run for a person to approve.