Back to Rivet

Workflows

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

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

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