Roll out an agent version
By the end of this tutorial you will have an agent whose new instructions went live through a staged rollout — served to a share of the traffic beside the old version, measured per run, and promoted in one call.
Five steps:
- Write a new version of the agent.
- Start the rollout — both versions serve traffic.
- Send traffic to the agent.
- See which version answered each run.
- Promote the new version, then prove it serves everything.
Every step is one API call, shown for all three clients. The ids in the responses are examples — copy the ones your own calls return.
Prerequisites
- A
nat_sk_…API key exported asNATURALI_TOKEN, and a client set up as in Create a provider. - The agent from Your first agent generation,
still at
version1, withPROJECTandAGENTexported.
export NATURALI_TOKEN=nat_sk_...
export PROJECT=proj_V1StGXR8Z5jdHi6B
export AGENT=agent_V1StGXR8Z5jdHi6B
1. Write a new version
Any write that changes the configuration archives a new version. Change the instructions so the two versions are easy to tell apart in a reply.
- CLI
- SDK
- curl
naturali patch-agent \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--instructions 'Answer in one sentence, then ask: Anything else I can help with?'
const { data: agent } = await naturali.agents.patchAgent({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
body: {
instructions:
'Answer in one sentence, then ask: Anything else I can help with?',
},
});
curl -X PATCH "https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "instructions": "Answer in one sentence, then ask: Anything else I can help with?" }'
{
"id": "agent_6a0WCYOBLQZOYvYz",
"name": "refund-explainer",
"instructions": "Answer in one sentence, then ask: Anything else I can help with?",
"version": 2,
"active_release": null
}
Version 1 is not gone. It is in the archive, and
GET /v1/projects/{project_id}/agents/{agent_id}/versions
lists both, each with the exact config it held:
{
"data": [
{ "version": 2, "config": { "instructions": "Answer in one sentence, then ask: Anything else I can help with?" } },
{ "version": 1, "config": { "instructions": "Answer in one sentence. If you are unsure, say so." } }
],
"total": 2
}
2. Start the rollout
PUT /v1/projects/{project_id}/agents/{agent_id}/release
serves two archived versions side by side: canary_percent of the traffic gets
canary_version, the rest stable_version.
- CLI
- SDK
- curl
naturali set-agent-release \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--stable-version 1 \
--canary-version 2 \
--canary-percent 50
const { data: agent } = await naturali.agentVersions.setAgentRelease({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
body: { stable_version: 1, canary_version: 2, canary_percent: 50 },
});
curl -X PUT "https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT/release" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "stable_version": 1, "canary_version": 2, "canary_percent": 50 }'
{
"id": "agent_6a0WCYOBLQZOYvYz",
"version": 2,
"active_release": {
"stable_version": 1,
"canary_version": 2,
"canary_percent": 50,
"promotion_gate": null
}
}
50 makes both sides visible in a few runs. In production you would start much lower.
3. Send traffic
Run the same question a few times. ?wait=true holds the call until the run
finishes, so the reply is in the response.
- CLI
- SDK
- curl
naturali create-agent-generation \
--project-id "$PROJECT" \
--agent-id "$AGENT" \
--wait true \
--messages '[{"role":"user","content":"What is our refund window?"}]'
const { data: generation } = await naturali.agents.createAgentGeneration({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
query: { wait: true },
body: {
messages: [{ role: 'user', content: 'What is our refund window?' }],
},
});
curl -X POST \
"https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT/generate?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "messages": [{ "role": "user", "content": "What is our refund window?" }] }'
Two of the four replies from this run, one per version:
{
"id": "gen_8ADWHOnm2JtQC42C",
"status": "completed",
"output": {
"content": "I am unsure, as I do not have access to your specific account information or terms of service."
}
}
{
"id": "gen_44nXn9OGAJaIr2wu",
"status": "completed",
"output": {
"content": "You have 30 days from the date of purchase to request a refund for eligible items. Anything else I can help with?"
}
}
The closing question gives the second one away, but a real change is rarely that visible. The next step reads the version from the record instead.
4. See which version answered
Every generation records the agent_version that
served it.
GET /v1/projects/{project_id}/generations
filtered by the agent lists them.
- CLI
- SDK
- curl
naturali list-generations \
--project-id "$PROJECT" \
--agent-id "$AGENT"
const { data: generations } = await naturali.generations.listGenerations({
path: { project_id: process.env.PROJECT! },
query: { agent_id: process.env.AGENT! },
});
curl "https://api.naturali.ai/v1/projects/$PROJECT/generations?agent_id=$AGENT" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"data": [
{ "id": "gen_44nXn9OGAJaIr2wu", "agent_version": 2, "status": "completed" },
{ "id": "gen_8ADWHOnm2JtQC42C", "agent_version": 1, "status": "completed" },
{ "id": "gen_CbbfP9Go13L5gRpP", "agent_version": 1, "status": "completed" },
{ "id": "gen_Cnxs18PPBFSzcYlP", "agent_version": 1, "status": "completed" }
],
"total": 4
}
Both versions served traffic. If yours all landed on one side, run step 3 a few more times — the split is random for these requests. This field is what you group by to compare the versions: their answers, their errors, their cost.
5. Promote the new version
POST /v1/projects/{project_id}/agents/{agent_id}/release/promote
makes the canary the agent's live configuration and ends the rollout.
- CLI
- SDK
- curl
naturali promote-agent-release \
--project-id "$PROJECT" \
--agent-id "$AGENT"
const { data: agent } = await naturali.agentVersions.promoteAgentRelease({
path: { project_id: process.env.PROJECT!, agent_id: process.env.AGENT! },
});
curl -X POST \
"https://api.naturali.ai/v1/projects/$PROJECT/agents/$AGENT/release/promote" \
-H "Authorization: Bearer $NATURALI_TOKEN"
{
"id": "agent_6a0WCYOBLQZOYvYz",
"instructions": "Answer in one sentence, then ask: Anything else I can help with?",
"version": 2,
"active_release": null
}
active_release is null: the rollout is over. Promoting again answers
409 NO_ACTIVE_RELEASE.
To prove it, run step 3 once more and repeat step 4. Every new run is version 2:
{
"data": [
{ "id": "gen_Vh6dwiWrfojIvxGb", "agent_version": 2, "status": "completed" }
]
}
If the new version had done worse,
POST /v1/projects/{project_id}/agents/{agent_id}/release/abort
ends the rollout the other way: the stable version's configuration goes back
live and all traffic returns to it. Neither call writes a new version.
What's next
- Score an agent change — measure the new version against a dataset before it gets any traffic.
- Agents → versioning and staged rollout
— restoring an old version, and
gating promotion on a passing eval
with
promotion_gate. - Sessions — the actor-based assignment that keeps one end user on one version.