Schema Migrations for Long-Lived AI Agents
Agent state survives longer than prompts. Version memories, plans, tool calls, and approvals so tomorrow's code can safely read yesterday's decisions.
Your agent remembers a task for three weeks. Your deployment remembers it for three seconds.
That mismatch is where ugly failures live. Schema migrations for long-lived AI agents must handle old memories, plans, approvals, tool calls, and checkpoints after the code that created them is gone. If you version only the database table, you have missed half the system.
Which Schemas Are Actually Persistent?
Map every artifact that can outlive one request:
| Artifact | Hidden compatibility risk |
|---|---|
| memory record | old scope or trust meaning |
| plan checkpoint | removed step type |
| tool call | renamed field or changed unit |
| approval | authority no longer valid |
| retrieval chunk | stale embedding or parser version |
| event log | new code interprets old event differently |
| model output | unversioned JSON shape |
Give each artifact an explicit schema_version. Do not infer version from timestamps or application releases. Hotfixes and backfills destroy that assumption.
Should You Migrate on Read or in Place?
Both patterns work, and both can hurt.
An in-place migration makes the active database consistent, simplifies queries, and fails visibly during deployment. It can also lock large tables, destroy original evidence, and make rollback hard.
Migration on read preserves old records and supports gradual rollout. It adds runtime complexity and can leave rarely accessed records broken for months.
The practical pattern is hybrid:
- write only the newest schema;
- read the newest plus a bounded set of older versions;
- transform old records through pure migration functions;
- backfill asynchronously;
- remove compatibility code only after measurement shows zero old reads.
How Do You Write a Safe Migrator?
Make every step deterministic, idempotent, and observable.
type MemoryV1 = { schema_version: 1; text: string; user_id: string };
type MemoryV2 = {
schema_version: 2;
content: string;
owner: { type: "user"; id: string };
scope: "global";
};
function v1ToV2(old: MemoryV1): MemoryV2 {
return {
schema_version: 2,
content: old.text,
owner: { type: "user", id: old.user_id },
scope: "global"
};
}
That scope: "global" default is a policy decision, not plumbing. If old records lack enough evidence, use an unknown state and require safer behavior. Never invent authority during migration.
Hash before and after records, count transforms by version, and keep a dead-letter path for items that cannot be upgraded automatically.
How Do Tool Schemas Evolve?
Tool changes are more dangerous than ordinary API changes because a model plans from the description. Renaming recipient to to can break stored plans even if the new tool works perfectly.
Keep versioned tool contracts:
{
"tool": "invoice.send",
"version": 3,
"arguments": {
"invoice_id": "inv_42",
"destination": {"type": "email", "value": "redacted"}
}
}
At execution time, validate that a stored plan’s tool version is still supported. If not, re-plan from current intent and current authorization. Do not silently map a destructive call when semantics changed.
How Do You Preserve Approval Meaning?
Approval is bound to a specific action, scope, destination, amount, and time. A migration that broadens any of those fields must invalidate the approval.
Store a digest of the human-visible proposal with the approval record. After migration, recompute the digest. If it differs, ask again. This is annoying and correct.
Permissions also expire independently of schema version. Revalidate current authority before resuming a task from an old checkpoint.
How Do You Roll Out Without Breaking Live Tasks?
Use dual readers before dual writers. Deploy code that understands old and new formats, then switch writes, then backfill. Add metrics for reads by schema version, migration failures, resumed-task failures, and approval invalidations.
Test with production-shaped snapshots that have been scrubbed of sensitive data. Include incomplete plans, duplicated events, missing optional fields, and records created during past incidents. Happy-path fixtures are useless here.
Run a shadow migrator first. It should transform and validate without committing. Compare counts and semantic invariants: same owner, same scope, same pending external action, and no new permission.
When Is the Migration Finished?
Not when the SQL command exits. It is finished when:
- all active readers understand the new version;
- new writes use only the new version;
- backfill errors are resolved;
- no old-version reads appear through the retention window;
- paused tasks resume safely;
- rollback has been tested or the migration is proven irreversible and accepted.
Long-lived agents turn application state into a durable behavioral contract. Version that contract explicitly. Migrate conservatively. When old data is ambiguous, reduce authority instead of guessing.
> Want more like this?
Get the best AI insights delivered weekly.
By subscribing, you agree to our Privacy Policy. You can unsubscribe at any time.
> Related Articles
AI Agent State Snapshots: Resume Long Jobs Without Repeating Side Effects
Durable agents need more than chat history. Snapshot plans, tool results, permissions, and idempotency state so a crash can resume safely instead of replaying the world.
Embedding Model Migration: Change Vectors Without Breaking Search
Embedding upgrades change the geometry of your index. Use versioned vectors, dual writes, shadow queries, and measured cutover instead of mixing incompatible representations.
LLM Request Coalescing: Stop Paying Twice for the Same Answer
When identical LLM requests arrive together, single-flight execution can collapse them into one upstream call—if cache keys, streaming, failures, and tenant boundaries are designed correctly.
Tags
> Stay in the loop
Weekly AI tools & insights.