Skip to content

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.

  1. 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-api
  2. Define 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"));});
  3. 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)
  4. 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_01JE0C3T
  5. Watch 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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

TypeScript
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.

Shell
docker run -p 7380:7380 \  -e DATABASE_URL=postgres://spindrel@db:5432/spindrel \  spindrel/engine:2.14.0

Stuck on step three? An engineer answers within one business day.

Talk to an engineer