# Steps

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

examples/docs/actors-workflows/try-step.ts:

```ts
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.

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

## Checkpoint-friendly loop design

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

examples/docs/actors-workflows/checkpoint-friendly.ts:

```ts
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 } });
```
