Run your first durable workflow
A workflow is a sequence of steps that survives deploys, restarts and retries. In this guide you'll install the Cinder SDK, connect a project, and trigger a job that sends a receipt email.
Already have a project?
Skip to step 2 and reuse your existing API key. Everything else works the same way.
Overview
Each step in a workflow is checkpointed. If a deploy, timeout or rate limit interrupts a run, Cinder restarts at the first unfinished step instead of the beginning. The example below has three moving parts:
A retry resumes at the last checkpoint, never at the start.
Before you begin
You'll need a recent Node.js runtime and a project to attach the workflow to. A sandbox project is enough for this guide.
- Runtime
- Node.js 20+
- Package manager
- npm, pnpm or bun
- Account
- Sandbox is fine
Create an API key in Project settings → Keys and keep it somewhere safe. You'll paste it into .env.local in a moment.
Install the SDK
Add the SDK to your project. It bundles the CLI, typed workflow helpers and the local runner, so one install covers the whole loop.
npm install @cinder/sdk
Using another package manager? Swap the command — the resulting lockfile is the only difference.
Connect a project
Create a config file at the repository root. The CLI and the runtime both read it, so local and deployed runs resolve the same project.
import { defineConfig } from "@cinder/sdk";
export default defineConfig({
project: "acme-receipts",
region: "eu-west-1",
retries: { attempts: 3, backoff: "exponential" },
});Keep your key out of git
Store the key as CINDER_SECRET in .env.local. Cinder reads it at runtime and never ships it to the browser bundle.
Link the directory once. This writes a project id to .cinder/project.json; commit that file, not the key.
npx cinder link --project acme-receipts
Run a workflow
Create the job. It waits for order.paid, loads the order and sends the receipt. Each unit of work is a named step, which is what makes the run resumable.
import { defineWorkflow } from "@cinder/sdk";
import { db } from "@/lib/db";
import { email } from "@/lib/email";
export const sendReceipt = defineWorkflow({
id: "send-receipt",
trigger: "order.paid",
run: async ({ event, step }) => {
const order = await step.run("load-order", () =>
db.orders.find(event.data.orderId),
);
await step.run("send-email", () =>
email.send({ to: order.email, template: "receipt", data: order }),
);
},
});Start the local runner and trigger the sample event from the console. You should see the steps complete in order.
$ npx cinder link Linked project acme-receipts (prj_4f21) $ npx cinder dev Cinder local runner ready on :4400 Watching src/jobs — 1 workflow registered ✓ send-receipt queued evt_8f2c1a 0.2s ✓ send-receipt completed run_91d7e0 2.8s
What just happened?
Cinder checkpointed load-order before calling the email provider. Kill the process mid-run, restart it, and the workflow resumes at send-email — no duplicate receipt.
Verify the setup
Three signals confirm the local loop is wired correctly. If any of them fails, run npx cinder doctor for a diff against the expected config.
| Check | Where | Status |
|---|---|---|
| SDK resolves the config | cinder.config.ts | Passing |
| Workflow appears in the runner | localhost:4400 | Passing |
| Run completes with a receipt | Runs → send-receipt | Passing |
Next steps
You have a working workflow. These three guides cover the parts most teams add next.
Was this page helpful?
Sorry to hear that. Tell us what was missing in the community channel and we'll fix it.