Concepts
Steps
Step boundaries, terminal failures as data, and checkpoint-friendly loops.
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.
Handling terminal failures as data
Use tryStep when a step failure should produce data instead of failing the whole workflow.
import { actor, setup } from "rivetkit";
import { workflow } from "rivetkit/workflow";
export const paymentActor = actor({
state: {
status: "pending" as "pending" | "manual-review" | "paid",
reason: null as string | null,
},
run: workflow(async (ctx) => {
const charge = await ctx.tryStep({
name: "charge-card",
maxRetries: 3,
run: async (ctx) => await chargeCard("order-123"),
});
await ctx.step("store-charge-result", async (step) => {
if (!charge.ok) {
step.state.status = "manual-review";
step.state.reason = charge.failure.error.message;
return;
}
step.state.status = "paid";
step.state.reason = null;
});
}),
actions: {
getState: (c) => c.state,
},
});
async function chargeCard(orderId: string): Promise<string> {
return `charge-${orderId}`;
}
export const registry = setup({ use: { paymentActor } });
Use try when you want to recover from terminal step, join, or race failures inside a named block.
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}`;
}
tryStepandtryonly catch terminal failures. Retry backoff, sleeps, queue waits, eviction, and history divergence still rethrow.- Catching a failure does not undo it.
stateandvarsmutations made before the failure remain visible aftertrySteportryreturns, so use explicit compensating steps when a caught failure needs cleanup. RollbackErroris not caught by default. Passcatch: ["rollback"]when you want rollback failures returned as data.
Checkpoint-friendly loop design
Use this when you need reliable replay and resume semantics across crashes and restarts.
import { actor, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";
type PaymentMessage = { id: string; amount: number };
export const checkpointFriendlyActor = actor({
state: {
appliedCount: 0,
totalAmount: 0,
lastPaymentId: null as string | null,
},
run: workflow(async (ctx) => {
await ctx.loop("payment-loop", async (loopCtx) => {
const [message] = await loopCtx.queue.nextBatch("wait-payment", {
timeout: 30_000,
});
if (!message) return;
const payment = message.body as PaymentMessage;
await loopCtx.rollbackCheckpoint("apply-payment-checkpoint");
const plan = (await loopCtx.step("build-plan", async (loopCtx) =>
buildPaymentPlan(payment),
)) as { paymentId: string; amount: number };
await loopCtx.step("apply-side-effects", async (step) => {
step.state.appliedCount += 1;
step.state.totalAmount += plan.amount;
step.state.lastPaymentId = plan.paymentId;
});
});
}),
actions: {
getState: (c) => c.state,
},
});
function buildPaymentPlan(payment: PaymentMessage): {
paymentId: string;
amount: number;
} {
return {
paymentId: payment.id,
amount: payment.amount,
};
}
export const registry = setup({ use: { checkpointFriendlyActor } });