Back to Rivet

Workflows

website/src/content/docs/actors/workflows.mdx

2.3.511.9 KB
Original Source

Use workflows for durable, multi-step execution with replay safety.

What are workflows?

A workflow is a durable, replayable run handler for a Rivet Actor.

  • Survives restarts: workflow progress is saved automatically.
  • Re-runs safely: replay follows the same recorded steps.
  • Event-driven: workflows can pause for queue messages, then continue.

Getting started

Simple workflow

Use this when you need a short multi-step sequence.

<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/simple-workflow/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/simple-workflow/client.ts" title="client.ts" /> </CodeGroup>

Loops

This is the recommended workflow shape for most actor workloads.

  • Use a queue wait inside the loop to receive the next unit of work.
  • Keep actor state changes in a single workflow loop.
  • This gives you one durable workflow that manages all actor progress.
<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/loops/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/loops/client.ts" title="client.ts" /> </CodeGroup>

Setup & teardown

Use this when the workflow should initialize resources, process queued commands, then clean up.

<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/setup-teardown/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/setup-teardown/client.ts" title="client.ts" /> </CodeGroup>

Features

Queue

Use this for fire-and-forget commands where the client does not need a reply.

Use the Loops example above as the baseline pattern.

Request/response (using queue)

Use this when the caller needs a response from queued processing.

<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/request-response/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/request-response/client.ts" title="client.ts" /> </CodeGroup>

Timers

Use queue messages as the trigger source, then sleep durably inside the workflow.

<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/timers/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/timers/client.ts" title="client.ts" /> </CodeGroup>

Join

Use join when several independent tasks can run in parallel.

<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/join/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/join/client.ts" title="client.ts" /> </CodeGroup>

Race

Use race when you need first-winner behavior.

<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/race/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/race/client.ts" title="client.ts" /> </CodeGroup>

Timeouts

Use step timeouts and retries for slow or flaky dependencies.

Step timeouts are critical by default and fail immediately. Set retryOnTimeout: true if a timeout should retry like any other error using maxRetries.

Workflows use roll-forward semantics everywhere. When a step throws, any state or vars mutations it made before failing are never rolled back, whether the step retries or the failure is caught by tryStep or try. The next attempt observes whatever the failed attempt already wrote, so write steps idempotently: check before you increment, or move the mutation after the fallible work.

<CodeSnippet file="examples/docs/actors-workflows/timeouts.ts" />

Handling terminal failures as data

Use tryStep when a step failure should produce data instead of failing the whole workflow.

<CodeSnippet file="examples/docs/actors-workflows/try-step.ts" />

Use try when you want to recover from terminal step, join, or race failures inside a named block.

ts
async function runPaymentFlow(ctx: any) {
  return await ctx.try("payment-flow", async (blockCtx: any) => {
    const auth = await blockCtx.step("authorize", async (blockCtx) =>
      authorizeOrder("order-123"),
    );
    const capture = await blockCtx.step("capture", async (blockCtx) =>
      captureOrder("order-123"),
    );
    return { auth, capture };
  });
}

async function authorizeOrder(orderId: string): Promise<string> {
  return `auth-${orderId}`;
}

async function captureOrder(orderId: string): Promise<string> {
  return `capture-${orderId}`;
}
  • tryStep and try only catch terminal failures. Retry backoff, sleeps, queue waits, eviction, and history divergence still rethrow.
  • Catching a failure does not undo it. state and vars mutations made before the failure remain visible after tryStep or try returns, so use explicit compensating steps when a caught failure needs cleanup.
  • RollbackError is not caught by default. Pass catch: ["rollback"] when you want rollback failures returned as data.

Error hooks

Use onError when you want a best-effort notification for workflow failures.

  • Step failures include the attempt number, retry counts, whether the step will retry, and the next retry delay.
  • Workflow failures also include terminal errors outside steps, such as rollback failures or code/history mismatches.
  • The hook is observational. It is not part of workflow replay, so use it for logging, metrics, or updating non-critical actor state.
  • This is also a good place to forward workflow failures to Sentry or another error reporting pipeline.
<CodeSnippet file="examples/docs/actors-workflows/error-hooks.ts" />

Rollback

Use rollback checkpoints before steps that have compensating actions.

<CodeSnippet file="examples/docs/actors-workflows/rollback.ts" />

Patterns

Store workflow progress in state + broadcast

Store progress in state so replay and recovery always restore it. Broadcast state changes so clients can render progress in realtime.

<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/store-progress/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/store-progress/client.ts" title="client.ts" /> </CodeGroup>

Cron (queue-driven)

Rivet scheduling triggers actions. For cron-like workflows, use a small scheduled action as a bridge that enqueues work, then process that work in the workflow loop.

<CodeSnippet file="examples/docs/actors-workflows/cron.ts" />

These are common workflow shapes used in production systems.

Queue-driven worker

Use this when external systems enqueue work and the actor should process each item durably.

<CodeSnippet file="examples/docs/actors-workflows/queue-worker.ts" />

Setup & teardown

Use this when you need one-time initialization before a long-lived loop, plus cleanup when the actor stops sleeping or is destroyed.

<CodeSnippet file="examples/docs/actors-workflows/setup-teardown-pattern.ts" />

Human approval gate

Use this when an operation must pause for a user or system decision before continuing.

<CodeSnippet file="examples/docs/actors-workflows/approval-gate.ts" />

Fan-out / fan-in (join)

Use this when independent work items can run in parallel and you need a single merged result.

<CodeSnippet file="examples/docs/actors-workflows/fan-in-out.ts" />

Batch drainer

Use this when throughput matters and handling one message at a time is too expensive.

<CodeSnippet file="examples/docs/actors-workflows/batch-drainer.ts" />

Coordinator -> worker RPC

Use this when one actor orchestrates work by calling actions on other actors.

<CodeSnippet file="examples/docs/actors-workflows/coordinator-worker.ts" />

Request/response over queue (async RPC)

Use this when you want decoupled actor-to-actor communication with durable waits and explicit completion.

<CodeGroup> <CodeSnippet file="examples/docs/actors-workflows/request-response-queue/index.ts" title="index.ts" /> <CodeSnippet file="examples/docs/actors-workflows/request-response-queue/client.ts" title="client.ts" /> </CodeGroup>

Scatter-gather across actors

Use this when multiple actors can process independent parts of a request in parallel, then return a merged response.

<CodeSnippet file="examples/docs/actors-workflows/scatter-gather.ts" />

Timeout + fallback actor

Use this when a primary actor call might be slow or unavailable and you need a deterministic fallback path.

<CodeSnippet file="examples/docs/actors-workflows/timeout-fallback.ts" />

Cross-actor saga (compensating actions)

Use this when a workflow spans multiple actors and each side effect may need compensation.

<CodeSnippet file="examples/docs/actors-workflows/cross-actor-saga.ts" />

Signal-driven control loop

Use this when workflow progress should be triggered by commands/events instead of fixed polling intervals.

<CodeSnippet file="examples/docs/actors-workflows/signal-control-loop.ts" />

Poll + backoff loop

Use this when an external dependency has variable availability and retries should slow down after failures.

<CodeSnippet file="examples/docs/actors-workflows/poll-backoff.ts" />

Child worker orchestration

Use this when one workflow coordinates many child workers (actors or worker workflows) and manages their lifecycle.

<CodeSnippet file="examples/docs/actors-workflows/child-worker-orchestration.ts" />

Bounded drain + concurrency cap

Use this when inbound work can spike and you need predictable per-iteration limits.

<CodeSnippet file="examples/docs/actors-workflows/bounded-drain.ts" />

Versioned workflow evolution

Use this when workflow structure changes across deployments and old histories must still replay.

<CodeSnippet file="examples/docs/actors-workflows/versioned-workflow.ts" />

Version gates with getVersion

Use ctx.getVersion(name, latest) to branch behavior when you change a workflow's logic while old instances are still in flight. It returns the version this instance is pinned to at that point:

  • A fresh instance resolves to latest.
  • An instance that already executed past this point under older code resolves to 1 (the implicit floor).

The resolved version is recorded in history, so replays are deterministic and each instance stays on the branch it started on. Inside a loop, every iteration resolves independently, so in-flight iterations finish on the old branch while new iterations pick up latest.

<CodeSnippet file="examples/docs/actors-workflows/get-version.ts" />

latest must be an integer >= 1, and the gate name must be unique within its scope like any other entry. Once every old instance has drained, retire the gate by replacing the call with ctx.removed(name, "version_check").

Checkpoint-friendly loop design

Use this when you need reliable replay and resume semantics across crashes and restarts.

<CodeSnippet file="examples/docs/actors-workflows/checkpoint-friendly.ts" />

Migrations

  • Keep workflow entry names stable once deployed.
  • If an old entry was removed or renamed, call ctx.removed(name, originalType).
  • To change behavior at a point while old instances are still running, gate it with ctx.getVersion(name, latest) (see Version gates).
  • This keeps replay compatible across deployments.

Step-only access to actor APIs

state, vars, db, client(), and connection/event APIs are only valid inside ctx.step(...) callbacks.

Use non-step workflow code for orchestration only: queue waits, sleeps, loops, joins, races, and rollback boundaries. Keep actor-local side effects in steps.

Debugging

  • GET /inspector/workflow-history returns workflow history status for an actor.
  • Response includes isWorkflowEnabled and history.
  • In non-dev mode, inspector endpoints require authorization.

Recommendations

  • Prefer queue-driven loops for long-lived workflows.
  • Structure long-lived workflows with setup and teardown around the main loop.
  • Keep actor state changes and side effects inside steps.
  • Store workflow progress in state and broadcast updates as progress changes.
  • Use timeouts and rollback for external side effects.
  • Write step bodies idempotently. state and vars mutations from a failed attempt are never rolled back, whether the step retries or tryStep/try catches the failure.