Rotor has two ways to run your code. Knowing which to use will save you time.
Quick answer
| If you want to… | Use |
|---|---|
| Call an existing HTTP endpoint you already have | Callback mode |
| Build a new multi-step workflow with retries, sleeps, and waits | Durable Workflows |
| Replace n8n / Make / Zapier flows | Callback mode |
| Replace Inngest / Temporal / Trigger.dev workflows | Durable Workflows |
| Keep your code on your own server | Callback mode |
| Write workflow logic once without managing retry state | Durable Workflows |
Callback mode
You own an HTTP endpoint. Rotor calls it with the job payload. Your endpoint runs your logic and returns 2xx. That's it.
[Job enqueued] → Rotor → POST your-app.example.com/handler → [Your code runs]
Rotor handles: delivery, retries on non-2xx, dead-letter queue, HMAC signature verification, concurrency limits, cron scheduling.
You handle: running a server, verifying the signature, writing idempotent handlers.
When to use it:
- You already have an HTTP server (Next.js API route, Hono, Express, anything)
- You're replacing scheduled Lambda functions, Vercel Cron handlers, or n8n/Make nodes
- You want Rotor to act as the queue and scheduler while your app stays stateless
- The job is a single unit of work (enrich this contact, send this email, sync this record)
// Your handler — receives a signed POST for each job
app.post("/rotor/enrichment", async (c) => {
const body = await c.req.text();
if (!verifyRotorSignature(body, c.req.header("x-rotor-signature"), secret)) {
return c.text("unauthorized", 401);
}
const { contactId } = JSON.parse(body);
await enrichContact(contactId);
return c.json({ ok: true });
});See the Quickstart for a full walkthrough.
Durable Workflows
You write a TypeScript function with step.* calls. Rotor checkpoints each step to Postgres. If the function retries, already-completed steps return their cached results — no double-execution.
[Event published] → Rotor → [Step 1 runs] → [Checkpoint] → [Step 2 runs] → [Checkpoint] → ...
Rotor handles: checkpointing, replay on retry, sleeping without occupying a worker, waiting for external events.
You handle: writing the workflow function, deploying a small server that serveWorkflow() runs on.
When to use it:
- The job has multiple steps with independent retry budgets
- You need
step.sleep("24h")— pause for a day without a running process - You need
step.waitForEvent— pause until something happens externally (email opened, payment received) - You need
step.waitForSignal— pause until a human approves - You need
step.invoke— fan out to sub-workflows and collect results
export const outreachSequence = createFunction(
{ id: "outreach-sequence", trigger: { event: "contact.added" } },
async ({ event, step }) => {
const profile = await step.run("fetch-profile", () =>
fetchContact(event.data.contactId)
);
await step.sleep("warm-up", "24h"); // no worker occupied during this wait
await step.run("send-email", () =>
sendEmail(profile.email, "intro")
);
const opened = await step.waitForEvent<{ at: string }>("wait-open", {
event: "email.opened",
match: `data.contactId == "${event.data.contactId}"`,
timeout: "7d",
});
if (opened) {
await step.run("send-followup", () => sendEmail(profile.email, "followup"));
}
}
);See the Workflows Quickstart for setup.
Can you use both?
Yes. They're independent. A common pattern:
- Use callback mode for high-volume single-step jobs (contact enrichment, HubSpot sync)
- Use durable workflows for multi-step sequences that need to sleep, wait, or branch
A durable workflow step (step.run) can enqueue a callback-mode job via the SDK if needed — for example, delegating the heavy lifting of an enrichment step to a callback queue that runs with higher concurrency.
Decision checklist
Pick callback mode if all of these are true:
- The job is a single unit of work (one action, one response)
- You already have or want a simple HTTP server
- You don't need to pause mid-job for more than a retry delay
Pick durable workflows if any of these are true:
- The job has 2+ sequential steps that should retry independently
- You need to wait hours or days between steps
- You need to pause until an external event (user action, approval, webhook) arrives
- You're building something that would require a state machine if you wrote it yourself