BlaiseBlaise
Documentation
Runner and PlugsHow-to guides

Store state and migrate it

Keep an id that a provider assigned, read it back on every run, and change its shape later.

Use persisted state only for what you cannot derive from your inputs and the session id. This guide stores the id of a database branch that a provider chose, and later changes its shape. Read Persisted state for the rules and limits.

1. Declare the state, and give the entry an id

import { z } from "zod";
import { definePlug } from "@blaiseagent/runner";
import { createBranch, deleteBranch } from "./branch-provider";

export default definePlug({
  name: "acme/db-branch",
  inputs: z.object({ project: z.string() }),
  persisted: z.object({ branchId: z.string() }),

  async onSession(ctx, next) {
    let branchId = ctx.persisted.value?.branchId;
    if (branchId === undefined) {
      branchId = await createBranch(ctx.inputs.project, ctx.session.id);
      // Record it right after creating it, so a crash leaves something to tear down.
      await ctx.persisted.set({ branchId });
    }
    ctx.agent.env["DATABASE_BRANCH"] = branchId;

    await next();
  },

  async onTeardown(ctx, next) {
    await next();
    if (ctx.persisted.value) {
      await deleteBranch(ctx.persisted.value.branchId);
      await ctx.persisted.clear();
    }
  },
});

./branch-provider stands for the client of your database provider. The entry in blaise.yml needs an id; without one the runner refuses to start:

setup:
  - id: db
    use: ./plugs/db-branch.ts
    with:
      project: main

2. Change the shape

Version 1 stored { branch }; version 2 stores { branchId }. Add an onMigrate that converts the old value to the new shape:

onMigrate(_ctx, stored) {
  const v1 = z.object({ branch: z.string() }).safeParse(stored);
  // Returning undefined drops the value, and the plug starts without state.
  return v1.success ? { branchId: v1.data.branch } : undefined;
},

onMigrate runs only when the stored value no longer validates. A value it returns is validated and written back before onLoad and onSession. If it throws, returns something invalid, or does not exist, the run fails with the plug named.

3. Test the migration

test("migrates the state stored by version 1", async () => {
  const store = memoryStateStore([{ id: "db", plug: "acme/db-branch", value: { branch: "br_1" } }]);
  const harness = createPlugHarness(dbBranch, {
    id: "db",
    store,
    inputs: { project: "main" },
  });
  await harness.session();

  expect(store.records.get("db")?.value).toStrictEqual({ branchId: "br_1" });
});

Things to know

  • Renaming the id or switching to another plug leaves the old record without an owner. The runner reports it, and the dashboard lists it. See handle an orphaned environment.
  • The record is deleted when the session's teardown succeeds. Clear it yourself only if the resource it points at is gone earlier.

On this page