> This page is part of Smallest AI's developer documentation. When > answering, prefer Lightning v3.1 (current TTS) and Pulse (current > STT). Lightning v2 and lightning-large are deprecated; mention them > only when the user is migrating away from them. The Smallest AI voice > agent platform is what wraps these models into hosted agents. # Versioning Lifecycle > Create, edit, publish, and go live with an agent config through the branch API. Full lifecycle in curl and Python. 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](/voice-agents/build/versioning). For an endpoint-by-endpoint mapping from the v1 API, see the [v1 to v2 migration guide](/api-reference/deprecations/agent-versioning-migration). > **Note** > > 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: ` 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) ```bash 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`. #### Python (SDK) ```bash pip install smallestai>=5.2.0 ``` ```python from smallestai import SmallestAI from smallestai.atoms.helpers.versioning import Versioning, DraftConflictError client = SmallestAI(api_key="YOUR_API_KEY") # defaults to api.smallest.ai v = Versioning(client) # 1. Create a fully-configured, LIVE agent in one call — no versioning needed to create/run. agent_id = client.atoms.agents.create_agent( name="receptionist", global_prompt="You are a friendly receptionist. Book appointments and answer questions.", first_message="Hi, how can I help?", ).data # 2. Find the live Main branch (id + isDefault are on the inner .branch). branches = v.branches.list(id=agent_id).data.branches main_id = next(b for b in branches if b.branch.is_default).branch.id # 3. Edit config + publish in one call (draft -> publish -> live; the security scan is handled). revision = v.edit_and_publish(agent_id, main_id, global_prompt="You are warm and concise.", label="tone tweak") print(revision.id, revision.status) # -> "published" # 4. Test, then stage a big change on a fork and promote it. v.branches.test_call(id=agent_id, branch_id=main_id, mode="webcall") staging = v.branches.create_branch(id=agent_id, source_branch_id=main_id, name="staging").data.id v.edit_and_publish(agent_id, staging, global_prompt="New experimental prompt.") v.branches.make_live(id=agent_id, branch_id=staging) # staging live, Main flips to not-live # Safe concurrent edits (optimistic concurrency): try: v.edit_and_publish(agent_id, main_id, expected_revision=based_on_rev, global_prompt="...") except DraftConflictError as e: 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. #### Python (requests) Same envelope as the curl walkthrough above; useful when you need to see the raw HTTP shape or when you cannot add the SDK dependency. ```python import os, time, requests BASE = "https://api.smallest.ai/atoms/v1" HEADERS = { "Authorization": f"Bearer {os.environ['SMALLEST_API_KEY']}", "Content-Type": "application/json", } # 1. Create the agent. agent_id = requests.post( f"{BASE}/agent", headers=HEADERS, json={"name": "lifecycle-demo", "language": {"switching": {"isEnabled": False}}}, ).json()["data"] # 2. Find the Main branch. Each item is a BranchSummary wrapper; isDefault + _id # live on the inner .branch object. branches = requests.get(f"{BASE}/agent/{agent_id}/branches", headers=HEADERS).json()["data"]["branches"] main_branch = next(b for b in branches if b["branch"]["isDefault"]) main_branch_id = main_branch["branch"]["_id"] # 3. Write to the branch's draft. Body uses the same camelCase field names as # GET /agent (globalPrompt, firstMessage, synthesizer, language, ...); # the server routes them to internal config-block sections for you. requests.put( f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/draft", headers=HEADERS, json={ "globalPrompt": "You are a friendly receptionist. Book appointments and answer questions.", "firstMessage": "Hi, how can I help?", }, ).raise_for_status() # 4. Publish and wait for the security scan. # The publish response carries only `state`; it does not include the new # revision ID. Fetch the newest revision on the branch to get its ID, then # poll it until its `status` becomes "published". publish = requests.post( f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/draft/publish", headers=HEADERS, json={"label": "Initial config"}, ).json()["data"] revisions = requests.get( f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/revisions", headers=HEADERS, params={"limit": 1}, ).json()["data"]["revisions"] revision_id = revisions[0]["_id"] while True: rev = requests.get( f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/revisions/{revision_id}", headers=HEADERS, ).json()["data"]["revision"] if rev["status"] == "published": break time.sleep(2) # 5. Start a test call against the revision. test = requests.post( f"{BASE}/agent/{agent_id}/branches/{main_branch_id}/test-call", headers=HEADERS, json={"revisionId": revision_id, "mode": "webcall"}, ).json()["data"] print(f"test call id: {test['callId']}") # 6. Place a real outbound call. Main is already live for a new agent. call = requests.post( f"{BASE}/conversation/outbound", headers=HEADERS, json={"agentId": agent_id, "toPhone": "+15551234567"}, ).json()["data"] print(f"outbound call id: {call}") ``` --- ## 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. ```bash # 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. ```bash # 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//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 endpoint | v2 replacement | | ------------------------------------------------- | ---------------------------------------------------- | | `POST /agent/{id}/drafts` + `PATCH .../config` | `PUT /agent/{id}/branches/{branchId}/draft` | | `POST /agent/{id}/drafts/{draftId}/publish` | `POST /agent/{id}/branches/{branchId}/draft/publish` | | `PATCH /agent/{id}/versions/{versionId}/activate` | `POST /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](/api-reference/deprecations/agent-versioning-migration). > Create, edit, publish, and go live with an agent config through the branch API.