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 haveCallback mode
Build a new multi-step workflow with retries, sleeps, and waitsDurable Workflows
Replace n8n / Make / Zapier flowsCallback mode
Replace Inngest / Temporal / Trigger.dev workflowsDurable Workflows
Keep your code on your own serverCallback mode
Write workflow logic once without managing retry stateDurable 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