Versioning Lifecycle

View as Markdown

Every Atoms agent’s runtime config (prompt, tools, voice, language, post-call metrics) is managed as revisions on branches. You edit a branch as a draft, publish the draft as a new revision, and make the branch you want live. This guide walks the full lifecycle from code.

For the dashboard equivalent and a deeper conceptual overview, see Agent Versioning. For an endpoint-by-endpoint mapping from the v1 API, see the v1 to v2 migration guide.

All examples assume BASE="https://api.smallest.ai/atoms/v1" and SMALLEST_API_KEY is set in your environment.


The flow

1. Create the agent POST /agent
2. Find the Main branch GET /agent/{agentId}/branches
3. Write to the branch's draft PUT /agent/{agentId}/branches/{branchId}/draft
4. Publish the draft POST /agent/{agentId}/branches/{branchId}/draft/publish
5. (Optional) test call POST /agent/{agentId}/branches/{branchId}/test-call
6. Make the branch live POST /agent/{agentId}/branches/{branchId}/live
7. Place a real call POST /conversation/outbound

A few points worth internalizing before you touch the API:

  • Every agent starts with a Main branch, and Main is live on day one. Main’s Version 1 is the config the agent serves.
  • Edits on any branch stack up as a single draft on that branch. You call PUT .../draft as many times as you want; each call merges into the same open draft.
  • Optimistic concurrency: pass expectedRevision: <draftRevision> on PUT .../draft to get a field-level conflict check. If another edit changed the same leaf field since, the server returns 409 with a DraftConflictError body listing the conflicting paths. Omit expectedRevision for last-write-wins.
  • Publish commits the draft as a revision. Revisions are immutable.
  • Every publish runs a security scan. If the draft already passed (a re-publish) or the workflow type is non-scannable, the commit happens synchronously and you get 200 with state: "committed". Otherwise the scan is armed and you get 202 with state: "scanning"; commit happens on scan pass. A FAILED draft is rejected with 409 (edit and re-publish).
  • Publishing on the live branch pushes to production immediately. Publish on a non-live branch first if you want to stage.
  • Restore does not touch the draft slot. It takes an older revision’s config and publishes it as a new revision at the head of the branch. So a branch with v1, v2, v3 restored to v1 ends up with v1, v2, v3, v4, where v4’s content matches v1 and v4 is the head. If the source revision’s security check failed or errored, restore is rejected with 403.
  • Test-call target: send includeDraft: true to test the open draft, revisionId to test a specific committed revision, or omit both to test the branch’s committed head.

Full walkthrough (curl)

$export BASE=https://api.smallest.ai/atoms/v1
$export AUTH="Authorization: Bearer $SMALLEST_API_KEY"
$export CT="Content-Type: application/json"
$
$# 1. Create the agent.
$AGENT_ID=$(curl -s -X POST "$BASE/agent" -H "$AUTH" -H "$CT" \
> -d '{"name": "lifecycle-demo", "language": {"switching": {"isEnabled": false}}}' \
> | jq -r '.data')
$echo "agent: $AGENT_ID"
$
$# 2. Find the Main branch. Each item in .data.branches is a BranchSummary
$# wrapper: {branch, isLive, hasOpenDraft, revisionsCount, headRevisionNumber}.
$# isDefault sits on the inner .branch, along with the branch _id.
$MAIN_BRANCH_ID=$(curl -s "$BASE/agent/$AGENT_ID/branches" -H "$AUTH" \
> | jq -r '.data.branches[] | select(.branch.isDefault==true) | .branch._id')
$echo "main branch: $MAIN_BRANCH_ID"
$
$# 3. Write to the branch's draft. This upserts the open draft; if there is no
># open draft, one is created. You can call this multiple times to build up
># the draft before publishing.
>#
># The body uses the same camelCase field names you see on GET /agent
># (globalPrompt, firstMessage, synthesizer, language, etc.). The server maps
># these into internal config-block sections (workflow_prompt, llm, voice, ...)
># for you; you don't send those section names directly.
$curl -s -X PUT "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/draft" \
> -H "$AUTH" -H "$CT" \
> -d '{
> "globalPrompt": "You are a friendly receptionist. Book appointments and answer questions.",
> "firstMessage": "Hi, how can I help?"
> }' | jq
$
$# 4. Publish the draft. Optional label shows in the UI as the revision name.
$# The publish response carries only `state` (scanning or committed); it does
$# not include the new revision ID. Fetch the newest revision on the branch
$# to get its ID.
$PUBLISH_RESP=$(curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/draft/publish" \
> -H "$AUTH" -H "$CT" \
> -d '{"label": "Initial config"}')
$
$STATE=$(echo "$PUBLISH_RESP" | jq -r '.data.state')
$REVISION_ID=$(curl -s "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/revisions?limit=1" \
> -H "$AUTH" | jq -r '.data.revisions[0]._id')
$echo "publish state: $STATE, revision: $REVISION_ID"
$
$# On the revision doc, the lifecycle field is `status` (published / draft /
># archived) and the scan lifecycle is on `securityCheck.status` (queued /
># passed / failed). Poll until the revision transitions to published.
$while true; do
$ REV_STATUS=$(curl -s "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/revisions/$REVISION_ID" \
> -H "$AUTH" | jq -r '.data.revision.status')
$ [ "$REV_STATUS" = "published" ] && break
$ sleep 2
$done
$echo "revision is committed"
$
$# 5. Start a test call against the newly-published revision.
$curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/test-call" \
> -H "$AUTH" -H "$CT" \
> -d "{\"revisionId\": \"$REVISION_ID\", \"mode\": \"webcall\"}" | jq
$
$# 6. Main is already live for a new agent, so no /live call is needed here.
$# If you had published on a non-live branch, the corresponding call is:
$# curl -X POST "$BASE/agent/$AGENT_ID/branches/$BRANCH_ID/live" -H "$AUTH"
$
$# 7. Place a real outbound call.
$curl -s -X POST "$BASE/conversation/outbound" -H "$AUTH" -H "$CT" \
> -d "{\"agentId\": \"$AGENT_ID\", \"toPhone\": \"+15551234567\"}" | jq

Python walkthrough

Two ways to run the same lifecycle from Python: use the smallestai SDK (typed methods + a Versioning helper that wraps the publish-and-poll dance), or call the REST endpoints directly with requests.

$pip install smallestai>=5.2.0
1from smallestai import SmallestAI
2from smallestai.atoms.helpers.versioning import Versioning, DraftConflictError
3
4client = SmallestAI(api_key="YOUR_API_KEY") # defaults to api.smallest.ai
5v = Versioning(client)
6
7# 1. Create a fully-configured, LIVE agent in one call — no versioning needed to create/run.
8agent_id = client.atoms.agents.create_agent(
9 name="receptionist",
10 global_prompt="You are a friendly receptionist. Book appointments and answer questions.",
11 first_message="Hi, how can I help?",
12).data
13
14# 2. Find the live Main branch (id + isDefault are on the inner .branch).
15branches = v.branches.list(id=agent_id).data.branches
16main_id = next(b for b in branches if b.branch.is_default).branch.id
17
18# 3. Edit config + publish in one call (draft -> publish -> live; the security scan is handled).
19revision = v.edit_and_publish(agent_id, main_id,
20 global_prompt="You are warm and concise.", label="tone tweak")
21print(revision.id, revision.status) # -> "published"
22
23# 4. Test, then stage a big change on a fork and promote it.
24v.branches.test_call(id=agent_id, branch_id=main_id, mode="webcall")
25staging = v.branches.create_branch(id=agent_id, source_branch_id=main_id, name="staging").data.id
26v.edit_and_publish(agent_id, staging, global_prompt="New experimental prompt.")
27v.branches.make_live(id=agent_id, branch_id=staging) # staging live, Main flips to not-live
28
29# Safe concurrent edits (optimistic concurrency):
30try:
31 v.edit_and_publish(agent_id, main_id, expected_revision=based_on_rev, global_prompt="...")
32except DraftConflictError as e:
33 print("conflict — latest is", e.latest_revision) # rebase, or retry without expected_revision

Source of truth: examples/agent_versioning_lifecycle.py in the SDK repo.


Working with additional branches

Fork a branch when you want to stage a big change without touching production. The source branch must have at least one committed revision.

$# Fork Main into a staging branch.
$STAGING_ID=$(curl -s -X POST "$BASE/agent/$AGENT_ID/branches" \
> -H "$AUTH" -H "$CT" \
> -d "{\"sourceBranchId\": \"$MAIN_BRANCH_ID\", \"name\": \"staging\"}" \
> | jq -r '.data._id')
$
$# Edit and publish on staging. Real traffic keeps hitting Main.
$curl -s -X PUT "$BASE/agent/$AGENT_ID/branches/$STAGING_ID/draft" \
> -H "$AUTH" -H "$CT" \
> -d '{"globalPrompt": "New experimental prompt."}' | jq
$
$curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$STAGING_ID/draft/publish" \
> -H "$AUTH" -H "$CT" -d '{"label": "Experimental v1"}' | jq
$
$# Once you are happy, make staging live. Main becomes not-live automatically.
$curl -s -X POST "$BASE/agent/$AGENT_ID/branches/$STAGING_ID/live" -H "$AUTH" | jq

Reverting

Restore republishes an older revision as a new revision at the head of the branch. If the branch is live, traffic switches immediately.

$# Find the revision you want to restore.
$curl -s "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/revisions?limit=20" -H "$AUTH" | jq
$
$# Restore it.
$curl -s -X POST \
> "$BASE/agent/$AGENT_ID/branches/$MAIN_BRANCH_ID/revisions/<REVISION_ID>/restore" \
> -H "$AUTH" | jq

Migrating from the v1 versioning API

If your integration calls /agent/{id}/drafts/* or /agent/{id}/versions/*, those are the v1 versioning endpoints. Move to the branch-and-revision endpoints:

v1 endpointv2 replacement
POST /agent/{id}/drafts + PATCH .../configPUT /agent/{id}/branches/{branchId}/draft
POST /agent/{id}/drafts/{draftId}/publishPOST /agent/{id}/branches/{branchId}/draft/publish
PATCH /agent/{id}/versions/{versionId}/activatePOST /agent/{id}/branches/{branchId}/live

Existing versionId values continue to resolve as revisionId, so IDs stored by earlier integrations keep working. Full endpoint-by-endpoint mapping and 409-handling patterns live in the v1 → v2 migration guide.