# Versioning & Migrations

## Versioned workflow evolution

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

examples/docs/actors-workflows/versioned-workflow.ts:

```ts
import { actor, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";

export const versionedWorkflowActor = actor({
  state: {
    runs: 0,
  },
  run: workflow(async (ctx) => {
    await ctx.step("validate-v2", async (step) => {
      step.state.runs += 1;
    });

    await ctx.removed("validate-v1", "step");

    await ctx.loop("main-loop-v2", async (loopCtx) => {
        await loopCtx.sleep("idle", 500);
        await loopCtx.step("heartbeat-v2", async (step) => {
          step.state.runs += 1;
        });
      });
  }),
  actions: {
    getState: (c) => c.state,
  },
});

export const registry = setup({ use: { versionedWorkflowActor } });
```

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

examples/docs/actors-workflows/get-version.ts:

```ts
import { actor, setup } from "rivetkit";
import { workflow } from "rivetkit/workflow";

export const versionGateActor = actor({
  state: {
    processed: 0,
  },
  run: workflow(async (ctx) => {
    await ctx.loop("process-loop", async (loopCtx) => {
        // Gate the changed code path. Each loop iteration resolves its version
        // independently: an iteration that already ran under the old code
        // (in-flight across the deploy) resolves to version 1, while a fresh
        // iteration resolves to `latest` (2 here). The resolved value is pinned
        // in history, so replays stay deterministic.
        const version = await loopCtx.getVersion("process-message", 2);

        if (version === 1) {
          // Preserve the original behavior for in-flight iterations.
          await loopCtx.step("process-v1", async (step) => {
            step.state.processed += 1;
          });
        } else {
          // New behavior for iterations that begin after this deploy.
          await loopCtx.step("process-v2", async (step) => {
            step.state.processed += 1;
          });
        }

        await loopCtx.sleep("idle", 1_000);
      });
  }),
  actions: {
    getState: (c) => c.state,
  },
});

export const registry = setup({ use: { versionGateActor } });
```

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

## 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](#version-gates-with-get-version)).
- This keeps replay compatible across deployments.
