Development Choices

Migrate Persisted AI Agent State Safely

Author
Drew YoungwerthSoftware Engineer
Published
Section
AI Agents
Length
6 min read3 sources cited

Treat persisted agent state as a versioned database: expand the schema first, keep new code able to read the previous version, migrate state with idempotent steps, and remove compatibility only after old instances are gone. Test fresh, upgraded, interrupted, and mixed-version paths before making the new shape mandatory.

Prerequisites

State migration flow from reading a version through transforming and writing it before resuming
Version the state explicitly instead of inferring its shape.

Before changing the schema, inventory the records, fields, indexes, and serialized payloads that the agent reads or writes. Separate durable state from values that can be reconstructed; deciding what an agent should persist first reduces the migration surface.

You also need representative state written by the current production version, a way to restore or recreate test data, and the deployment plan: rolling, canary, or coordinated downtime. The plan determines how long two schema versions must coexist.

  1. Give the persisted state an explicit schema version

    Persisted agent state outlives the code version that wrote it, so deployment compatibility is a data migration problem. A run may pause under one release and resume after several deployments; treating its stored payload as an in-process object merely postpones the failure until deserialization or the next state transition.

    Store a schema version with the database or record, rather than inferring it from whichever fields happen to be present. Keep this version separate from the application release, prompt revision, and model name: those values answer different questions.

    Cloudflare’s Durable Objects storage guide, updated July 3, 2026, gives the relevant persistence boundary: in-memory state can disappear during eviction or deployment, while attached storage is private, transactional, and strongly consistent. The same rule applies outside Durable Objects: anything expected to survive a restart needs a versioned stored representation.

  2. Design an additive schema before changing readers

    Start with an expand step. Add the new field, table, or representation while leaving the previous one usable. New code must read both the previous schema and the new schema. During a rolling deployment, readers should tolerate the previous schema or new instances cannot resume runs saved by old instances.

    Compatibility must also work in the other direction while old instances remain live. If new code immediately deletes an old field or writes a shape that only it understands, an old instance selected after a rollback or during the rollout can no longer resume that run. Preserve the old representation, or make the expanded representation harmless to the old reader, until the overlap ends.

    This is the transition-phase pattern described in Evolutionary Database Design, accessed August 26, 2026: the database supports old and new access patterns simultaneously, then removes the old path later. The cost is concrete—another code branch, temporary duplicate data, and at least one later cleanup deployment. A one-step destructive change is suitable only when access can be stopped and every reader upgraded together. Check the overlap implied by the chosen agent deployment topology rather than assuming that condition holds.

  3. Write the schema change as a small, ordered migration

    Put each schema and data change under version control, assign it a unique order, and record successful application in persistent metadata. Keep the step small enough that its precondition and completed state can be checked directly.

    For SQLite, prefer an additive change when it fits. The SQLite ALTER TABLE documentation, accessed August 26, 2026, documents a limited set of direct operations: renaming a table or column, adding a column, and dropping a column. An added NOT NULL column requires a non-null default, and some added constraints are checked against existing rows. That makes a nullable expansion followed by a controlled backfill easier to stage than immediately imposing a constraint on old state.

    Do not combine expansion, destructive cleanup, and unrelated data repair in one migration. Combining them enlarges the failure boundary and prevents the old reader from surviving the intermediate deployment. The cost of smaller migrations is more version entries and deployment bookkeeping; the benefit is knowing exactly which transition failed.

  4. Make every migration step idempotent

    A migration must be idempotent because recovery can replay initialization after an interrupted upgrade. Checking only a schema-version number is insufficient if the process can stop after changing a table but before recording the new version.

    Make each effect conditional on the state it changes: add a field only when absent, backfill only rows that still need the derived value, create an index only when absent, and advance the recorded version only after the required effects have completed. Where the storage system permits it, apply the change and version update in one transaction. Otherwise, make every individual operation safe to repeat.

    This adds checks and may require a progress marker for a backfill, but it avoids a recovery path that depends on an operator guessing how far the first attempt reached. A migration that assumes exactly one execution is the wrong answer for initialization or resume code that the runtime may invoke again.

  5. Choose when migration runs and prevent concurrent use

    Run migration before the agent interprets or mutates the stored state. For a stateful object, that can mean migration during initialization while requests are blocked; Cloudflare documents blockConcurrencyWhile() for preventing requests from reaching a Durable Object until initialization completes. Do not release the object with half-migrated in-memory values.

    Migrate-on-resume suits stores split across many agent identities because inactive state is not touched until needed. Its costs are added latency on the first later resume and a long period in which old schemas remain present. An eager migration suits an enumerable central store when the team wants failures discovered before serving the new release. Its costs are deployment work and concentrated storage load. Neither choice removes the need for compatible readers during a rolling deployment.

  6. Test interruption and mixed-version deployment before cleanup

    Exercise the states the release will actually produce:

    Starting condition Required result
    Empty store Current schema is initialized once
    Previous schema New code migrates and resumes the run
    Current schema Initialization makes no further changes
    Migration replay Repeating initialization produces the same stored result
    Interruption after each migration effect Recovery completes without duplicate or missing data
    Old and new instances live together Both can resume state written during the overlap
    Rollback after expansion Old code still reads the expanded store

    Use stored fixtures rather than constructing every case through the current writer; otherwise the test may never contain a genuinely old representation. Verify the resumed state transition as well as the final columns. If the agent keeps an execution log, agent-run replay can expose a migration that preserved the shape but changed the meaning.

    Deploy the expanded schema and compatible reader first. Then run or allow the data migration while tracking migration version, failures, and resumed runs; correlation identifiers across agent runs make those events traceable. Remove the old field and compatibility branch only after no old instances remain, migrated state has been checked, and rollback no longer requires the old reader. Cleanup is a separate contract migration, not part of expansion.

Expected result

The deployed agent can initialize new state, resume state written by the previous release during a rolling deployment, and recover from migration interruption by replaying initialization. Stored versions advance in order, repeated migration attempts do not change an already migrated record, and the previous representation is removed only after every live reader has stopped depending on it.

Sources

  1. Cloudflare’s Durable Objects storage guidedevelopers.cloudflare.com
  2. Evolutionary Database Designmartinfowler.com
  3. SQLite `ALTER TABLE` documentationsqlite.org

See also