Agent Versioning Migration Guide (v1 → v2)

View as Markdown

The Atoms agent-versioning API moved to a branch-and-revision model. This page walks through what changed and gives you a concrete, endpoint-by-endpoint mapping to move existing integrations across.

What changed

The v1 model had two loose concepts (drafts and versions) that lived side-by-side on one linear history per agent. The v2 model wraps them in a branch:

  • Every editable copy of an agent is a branch. Each agent has a Main branch by default.
  • Each branch has one open draft at a time. Draft edits are upserts.
  • Publishing a draft commits a revision on the branch. Revisions are immutable and equivalent to the old version.
  • Exactly one branch per agent is live and serves production traffic. activate on a specific version is gone; you make a whole branch live.

Existing IDs carry over. A v1 versionId equals its migrated revisionId, so any code that stores version IDs can keep resolving them via the by-id endpoints (which are kept, see below).

Endpoint mapping

v1 endpointv1 status after launchv2 equivalent
POST /agent/{id}/drafts409 versioning_v2_migration_requiredPUT /agent/{id}/branches/{branchId}/draft (auto-opens a draft)
GET /agent/{id}/drafts409GET /agent/{id}/branches (each branch surface exposes openDraftId / hasOpenDraft)
GET /agent/{id}/drafts/{draftId}keptGET /agent/{id}/branches/{branchId}/draft
PATCH /agent/{id}/drafts/{draftId}409Not migrated. Rename is per-branch; drafts are no longer named.
DELETE /agent/{id}/drafts/{draftId}409DELETE /agent/{id}/branches/{branchId}/draft
PATCH /agent/{id}/drafts/{draftId}/config409PUT /agent/{id}/branches/{branchId}/draft
GET /agent/{id}/drafts/{draftId}/diffkeptGET /agent/{id}/diff?a=<branchId>:draft&b=<revisionId>
POST /agent/{id}/drafts/{draftId}/publish409POST /agent/{id}/branches/{branchId}/draft/publish
POST /agent/{id}/drafts/{draftId}/test-callkeptPOST /agent/{id}/branches/{branchId}/test-call with includeDraft: true
GET /agent/{id}/versions409GET /agent/{id}/branches/{branchId}/revisions
GET /agent/{id}/versions/{versionId}keptGET /agent/{id}/branches/{branchId}/revisions/{revisionId}
GET /agent/{id}/versions/diffkeptGET /agent/{id}/diff?a=<revisionId>&b=<revisionId>
PATCH /agent/{id}/versions/{versionId}409 (external)Metadata edits removed. Set label at publish time via POST .../draft/publish.
PATCH /agent/{id}/versions/{versionId}/activate409POST /agent/{id}/branches/{branchId}/live or POST /agent/{id}/branches/{branchId}/revisions/{revisionId}/restore
POST /agent/{id}/versions/{versionId}/test-callkeptPOST /agent/{id}/branches/{branchId}/test-call with revisionId

409 endpoints return a body of the form { "status": false, "error_type": "versioning_v2_migration_required", "errors": [...] } and set Deprecation: true. The kept endpoints continue to accept the same request shape and return the same response.

The SDK routes under /sdk/agents/* are unaffected by this migration.

Timeline

  • Launch day: v1 write endpoints and v1 list reads begin returning 409. Kept endpoints and the SDK routes continue to work. The v2 branch endpoints go live behind the same base URL.
  • Coexistence window: kept v1 endpoints (by-id reads + both test-calls) continue to work so existing integrations that store versionId values do not break. A v1 versionId equals its migrated revisionId, so by-id lookups continue to resolve.
  • Sunset: v1 endpoints are removed. No removal date is published yet. When the backend owner fixes a date, this page and the Deprecation Notices page will carry it, and the affected endpoints will start returning a Sunset: header (RFC 8594) with an RFC 7231 date.

Worked examples

1. Create a draft, edit it, publish it

Before (v1):

$DRAFT=$(curl -s -X POST "$BASE/agent/$AGENT_ID/drafts" \
> -H "$AUTH" -H "$CT" \
> -d "{\"sourceVersionId\": \"$ACTIVE_VERSION_ID\"}" | jq -r '.data._id')
$
$curl -s -X PATCH "$BASE/agent/$AGENT_ID/drafts/$DRAFT/config" \
> -H "$AUTH" -H "$CT" \
> -d '{"globalPrompt": "..."}'
$
$curl -s -X POST "$BASE/agent/$AGENT_ID/drafts/$DRAFT/publish" \
> -H "$AUTH" -H "$CT" -d '{"label": "..."}'

After (v2):

$# 1. Find the branch you want to publish on. `.data.branches[i].branch` is the
$# Branch object; `isDefault` / `isLive` / `hasOpenDraft` sit on the outer
$# BranchSummary wrapper.
$MAIN_BRANCH_ID=$(curl -s "$BASE/agent/$AGENT_ID/branches" -H "$AUTH" \
> | jq -r '.data.branches[] | select(.branch.isDefault==true) | .branch._id')
$
$# 2. Write to the branch's draft. If no draft is open, one is created.
># Body uses the same camelCase agent fields as GET /agent (globalPrompt,
># firstMessage, synthesizer, language, etc.).
>curl -s -X PUT "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/draft" \
> -H "$AUTH" -H "$CT" \
> -d '{"globalPrompt": "..."}'
>
># 3. Publish.
>curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/draft/publish" \
> -H "$AUTH" -H "$CT" -d '{"label": "..."}'

2. Activate an older revision

Before (v1):

$curl -X PATCH "$BASE/agent/$AGENT_ID/versions/$VERSION_ID/activate" -H "$AUTH"

After (v2): activate is replaced by two options:

  • Make the branch that owns the revision live. Sets the branch’s head as production traffic.

    $curl -X POST "$BASE/agent/$AGENT_ID/branches/$BRANCH_ID/live" -H "$AUTH"
  • Restore an older revision on the current branch. Republishes its config as a new revision at the head of the branch. Existing revisions keep their IDs. If the branch has v1, v2, v3 and you restore v1, the branch ends up with v1, v2, v3, v4, where v4’s content matches v1 and v4 becomes the head.

    $curl -X POST \
    > "$BASE/agent/$AGENT_ID/branches/$BRANCH_ID/revisions/$REVISION_ID/restore" \
    > -H "$AUTH"

3. List past versions

Before (v1):

$curl "$BASE/agent/$AGENT_ID/versions?limit=20" -H "$AUTH"

After (v2): list per branch.

$curl "$BASE/agent/$AGENT_ID/branches/$BRANCH_ID/revisions?limit=20" -H "$AUTH"

Revisions returned here have the same _id as v1 versionId.

4. Diff a draft against the active version

Before (v1):

$curl "$BASE/agent/$AGENT_ID/drafts/$DRAFT/diff" -H "$AUTH"

After (v2): diff endpoint takes two refs; each side is either a revisionId or <branchId>:draft.

$curl "$BASE/agent/$AGENT_ID/diff?a=$BRANCH_ID:draft&b=$ACTIVE_REVISION_ID" -H "$AUTH"

5. Test call against a draft or a specific revision

Before (v1): two different endpoints.

$# Draft
$curl -X POST "$BASE/agent/$AGENT_ID/drafts/$DRAFT/test-call" -H "$AUTH" \
> -d '{"mode":"webcall"}'
$# Version
$curl -X POST "$BASE/agent/$AGENT_ID/versions/$VERSION_ID/test-call" -H "$AUTH" \
> -d '{"mode":"webcall"}'

After (v2): one endpoint on the branch, differentiated by body.

$# Draft on this branch
$curl -X POST "$BASE/agent/$AGENT_ID/branches/$BRANCH_ID/test-call" -H "$AUTH" \
> -d '{"includeDraft": true, "mode": "webcall"}'
$# Specific revision on this branch
$curl -X POST "$BASE/agent/$AGENT_ID/branches/$BRANCH_ID/test-call" -H "$AUTH" \
> -d '{"revisionId": "'$REVISION_ID'", "mode": "webcall"}'

Handling the 409 responses

There are three flavors of 409 you can see on the branch-and-revision API. Discriminate on error_type (for deprecation) or on body shape (for concurrency and base-revision errors).

1. versioning_v2_migration_required (deprecated v1 endpoint)

Returned by any of the deprecated v1 write / list endpoints. Body:

1{
2 "status": false,
3 "error_type": "versioning_v2_migration_required",
4 "errors": [
5 "This endpoint is deprecated under the branch model. Use the /agents/:id/branches APIs — publish, make-live and config edits now happen on branches. If you are calling this via the MCP server, update the MCP tool to its branch-model version as well."
6 ]
7}

Header: Deprecation: true. Route to the v2 equivalent from the mapping table above.

2. DraftConflictError (concurrent draft edit)

Returned by PUT /agent/{id}/branches/{branchId}/draft when the caller passed expectedRevision and a concurrent edit changed the same field since. Body has a data.conflict object listing per-field diffs and both revision numbers.

Retry the read → merge → write cycle, or drop expectedRevision for last-write-wins.

3. base_revision_unavailable (unknown base for the draft)

Returned by PUT .../draft when expectedRevision names a revision the branch does not have. Body:

1{
2 "status": false,
3 "errors": ["base_revision_unavailable"]
4}

Fetch the branch’s current head revision (GET /agent/{id}/branches/{branchId}) and rebase the client’s expectedRevision before retrying.

Client sketch

Python:

1import requests
2
3# v1 → v2 migration path (deprecated endpoint case)
4resp = requests.post(f"{BASE}/agent/{agent_id}/drafts", headers=headers, json={...})
5
6if resp.status_code == 409 and resp.json().get("error_type") == "versioning_v2_migration_required":
7 branches = requests.get(f"{BASE}/agent/{agent_id}/branches", headers=headers).json()["data"]["branches"]
8 main = next(b for b in branches if b["branch"]["isDefault"])
9 resp = requests.put(
10 f"{BASE}/agent/{agent_id}/branches/{main['branch']['_id']}/draft",
11 headers=headers,
12 json={...},
13 )
14
15# Draft-edit conflict cases on the v2 PUT itself
16if resp.status_code == 409:
17 body = resp.json()
18 errors = body.get("errors") or []
19 if "base_revision_unavailable" in errors:
20 # Client's expectedRevision points at a revision this branch does not have.
21 # Refetch the branch head and rebase.
22 ...
23 elif body.get("data", {}).get("conflict"):
24 # Concurrent edit changed the same fields. Merge and retry.
25 ...
26
27resp.raise_for_status()

Node:

1const resp = await fetch(`${BASE}/agent/${agentId}/drafts`, {
2 method: "POST",
3 headers,
4 body: JSON.stringify({ /* ... */ }),
5});
6
7if (resp.status === 409) {
8 const body = await resp.json();
9 if (body.error_type === "versioning_v2_migration_required") {
10 const branches = (await fetch(`${BASE}/agent/${agentId}/branches`, { headers }).then(r => r.json())).data.branches;
11 const main = branches.find(b => b.branch.isDefault);
12 await fetch(`${BASE}/agent/${agentId}/branches/${main.branch._id}/draft`, {
13 method: "PUT",
14 headers,
15 body: JSON.stringify({ /* ... */ }),
16 });
17 } else if ((body.errors || []).includes("base_revision_unavailable")) {
18 // Refetch branch head, rebase expectedRevision, retry.
19 } else if (body?.data?.conflict) {
20 // Concurrent edit; merge and retry.
21 }
22}