Docs Getting started
Quickstart
From npm install to a workflow that survives kill -9, in five steps. Examples are TypeScript; Python and Go use the same names.
Install and log in
The CLI ships with the SDK. Logging in writes a project token to .spindrel/credentials, which is already in the generated .gitignore.
Shell npm i @spindrel/sdknpx spindrel loginnpx spindrel init --project shop-apiDefine a workflow
A workflow is an async function. Each step.run result is stored before the next line runs.
workflows/welcome.ts import { workflow, step } from "@spindrel/sdk";export const welcome = workflow("welcome", async (user: { id: string; email: string }) => { await step.run("sendWelcome", () => mail.send(user.email, "welcome")); await step.sleep("wait-a-day", "1d"); await step.run("sendTips", () => mail.send(user.email, "tips-01"));});Start a local worker
spindrel dev runs a worker against a local engine with hot reload. Nothing leaves your machine until you deploy.
Shell npx spindrel dev# engine http://localhost:7380# worker node-1 · 1 workflow registered (welcome)Start a run
Start runs from your API handler. The call returns as soon as the run is queued, usually in under 20 ms.
api/signup.ts import { spindrel } from "@spindrel/sdk";import { welcome } from "../workflows/welcome";const run = await spindrel.start(welcome, { id: user.id, email: user.email });console.log(run.id); // run_01JE0C3TWatch it
Tail a workflow from the terminal, or open the run in the dashboard for the waterfall and event history.
Shell npx spindrel runs tail welcome# run_01JE0C3T sendWelcome ok 96ms# run_01JE0C3T wait-a-day sleeping until 2026-10-23 14:20 UTC
Concepts
Steps
A step is the unit of checkpointing. Its return value must serialise to JSON. Give each step a name that stays stable across deploys; renaming one is a breaking change for runs in flight.
const user = await step.run("loadUser", () => db.users.find(id));Sleeps
step.sleep takes a duration or a Date. The worker is released immediately; sleeping runs cost one step, not compute time.
await step.sleep("cool-off", "36h");await step.sleep("renewal", new Date("2027-01-01T09:00:00Z"));Signals
Park a run until something outside it answers. Signals carry a JSON payload and are delivered in the order the engine receives them.
const decision = await step.waitForSignal<{ ok: boolean }>("review", { timeout: "72h" });// elsewhereawait spindrel.signal(runId, "review", { ok: true });Retries
Default: 3 retries with exponential backoff from 1 s. Throw NonRetryable to fail a step immediately, e.g. on a 4xx you know will not change.
await step.run("push", () => ledger.push(entry), { retries: 6, backoff: "exp",});Concurrency keys
Limit how many runs share a resource. Runs over the limit queue under their key, so one busy tenant does not slow the rest.
export const sync = workflow("sync", { concurrency: { key: (input) => `tenant:${input.orgId}`, limit: 4 },}, async (input) => pullAccounts(input));Self-hosting
The engine is one binary and a Postgres database. Point the SDK at it with SPINDREL_URL. The hosted dashboard is optional.
docker run -p 7380:7380 \ -e DATABASE_URL=postgres://spindrel@db:5432/spindrel \ spindrel/engine:2.14.0Stuck on step three? An engineer answers within one business day.
Talk to an engineer