Guides

Fork and snapshot primitives

Snapshot, fork or branch individual primitives to explore safely, then keep or discard the result.

Agents make risky changes: migrations, large refactors, multi-step plans that might not work out. Forking lets an agent try one in a full copy of the primitives it needs, then keep the result or throw it away. Every primitive forks on its own, so you copy exactly what the attempt touches: one database, one filesystem, one memory store. Nothing in the original changes until you decide to keep the work.

On Primitives Cloud, managed snapshots, forks and restore are on the Pro and Enterprise plans; directory forks (forkDir) and database branches work on every plan. Self-hosted, the open-source engine includes all of them.

What a fork copies

fork() copies one primitive, at one consistent moment, into a new primitive with a new id, in the same group or, with group, in another group. The target group must already exist: forking into one that doesn't fails with 404 not_found.

Primitivefork()Narrower option
DatabaseCopied; branches are notdb.branch(name)
FilesystemCopied; files over 64 KiB share contents instead of being copiedfs.forkDir({ from })
MemoryCopiedn/a
QueueCopied, including pending jobs; leased jobs are releasedn/a
VaultSecrets copied with their grants, re-encrypted under the target group's keyn/a
KVEvery key with its version and absolute expiryn/a
StorageEvery object with its metadata. The source's signed URLs don't work on the copyn/a

The copy records where it came from in forked_from: { id, name, type, group, snapshot }.

Pattern 1: snapshot, try, roll back

The cheapest safety net needs no copy at all. Snapshot the primitive, let the agent work on it, and restore if it goes wrong:

rollback.ts
const group = await p.groups.get('agent-42');
const orders = group.db('orders');

await orders.snapshot('pre-migration');
await runAgent({ task: 'Migrate the orders schema' });

if (!(await checksPass(orders))) {
  await orders.restore('pre-migration'); // only this database rolls back
}

Pattern 2: fork into a sandbox group for a sub-agent

Create a new group, fork just the primitives a sub-agent needs into it, and give the sub-agent an API key scoped to that group. The sub-agent can't reach anything you didn't fork: no other databases, no secrets.

explore.ts
const group = await p.groups.get('agent-42');
const repo = group.fs('repo');
const orders = group.db('orders');

const trial = await p.groups.create({ name: 'agent-42-orm-upgrade' });
const trialRepo = await repo.fork({ group: trial.name, name: 'repo' });
const trialDb = await orders.fork({ group: trial.name, name: 'orders' });

// A key created in the dashboard, scoped to agent-42-orm-upgrade and db_*, fs_*
await runSubAgent({
  mcpUrl: 'https://mcp.useprimitives.com/mcp',
  apiKey: process.env.ORM_UPGRADE_API_KEY,
  task: 'Upgrade the ORM and fix the tests',
});

const { rows } = await trialDb.sql`select count(*) as failing from test_runs where ok = 0`;
if (rows[0].failing === 0) {
  // keep it: replace the original with the trial's copy
  await orders.snapshot('pre-orm-upgrade');
  await orders.delete();
  await trialDb.moveTo('agent-42'); // same id, now agent-42's `orders`
}
await trial.destroy({ force: true }); // the sandbox group and whatever is left in it

Keys are scoped to a group by name, so create the sub-agent's key for the trial group's name, and revoke it in the dashboard when the work is done. There's no merge: keeping a fork means swapping it in for the original. Moving it keeps its id, so anything holding trialDb.id still works. Snapshot the original first if you might want it back, and note that deleting a primitive also deletes its snapshots. To keep a recovery point, fork the original aside (orders.fork({ name: 'orders-before-upgrade' })) before deleting it.

Pattern 3: fan out N attempts

Run several attempts in parallel and keep the best one:

fan-out.ts
const workspace = group.fs('workspace');
const attempts = await Promise.all(
  [1, 2, 3].map((i) => workspace.fork({ name: `workspace-attempt-${i}` })),
);

const scores = await Promise.all(attempts.map((fs) => solveAndScore(fs)));
const best = attempts[scores.indexOf(Math.max(...scores))];

await Promise.all(attempts.filter((fs) => fs !== best).map((fs) => fs.delete()));
// `best` is now the workspace to keep using, e.g. group.fs('workspace-attempt-2')

Pattern 4: fork just a directory

When a sub-agent only needs a scratch copy of a repository, fork the directory inside the same filesystem:

const workspace = group.fs('workspace');
const { path } = await workspace.forkDir({ from: '/repo', to: '/forks/attempt-1' });
await runSubAgent({
  apiKey: process.env.AGENT_42_FS_API_KEY, // scoped to agent-42 and fs_*
  task: `Work only inside ${path} of the workspace filesystem`,
});

// keep it: swap the directories
await workspace.move('/repo', '/forks/previous');
await workspace.move(path, '/repo');

Drop it with workspace.remove(path, { recursive: true }). The key above can still reach the whole group, so fork into a separate group (Pattern 2) when the sub-agent isn't trusted.

Pattern 5: branch just the database

const trial = await group.db('orders').branch('try-migration');
await trial.exec(migrationSql);
await trial.promote(); // or trial.discard()

Branch promotion is the only fork-style operation that merges back in place: it fails with 409 branch_conflict if the main database was written after the branch was created, unless you pass { force: true }.

From MCP

Agents can snapshot and fork with primitive_snapshot and primitive_fork, and branch with db_branch and fs_fork, when their API key allows them:

primitive_fork
{ "primitive": "orders", "new_name": "orders-try" }

A scoped API key can fork only within its own group, so an agent can't multiply its own access. Your orchestrator decides whether to fork into another group and give a sub-agent a key there.

From the CLI

primitives primitive list --group agent-42 --type db      # find the id of `orders`
primitives primitive snapshot $ORDERS_ID --label pre-migration
primitives primitive restore $ORDERS_ID pre-migration
primitives primitive fork $ORDERS_ID --name orders-try
primitives group create agent-42-sandbox
primitives primitive fork $REPO_ID --group agent-42-sandbox

Tips

  • Snapshot before you swap a fork in, so you can roll back with one call.
  • Fork only the primitives a sub-agent needs, into its own group. Leave the vault out for sub-agents you don't trust.
  • Give forks predictable names (<name>-<purpose>) and delete them when you're done.