Agents and versions
A versioned JSON document, and what appending a version means.
An agent is a document. agents.upsert writes it; every change appends a version
and a session is pinned to the version it started on, for life. So a prompt edit
cannot retroactively change what a conversation from last week was doing, and a
transcript is always explicable by a definition that still exists.
await client.agents.upsert({
slug: "support-bot",
definition: {
name: "Support bot",
description: "Answers order questions and opens returns.",
model: {
provider: "gateway", // a provider *name*, not a type
model: "anthropic/claude-sonnet-4.6",
reasoning: "low",
fallback: { models: [{ model: "openai/gpt-5.6-luna" }] }
},
system: "You are a support agent. Look things up before answering.",
tools: [
{ type: "mcp", connection: "support-bot", allow: ["lookup_order", "create_return"] }
],
policies: {
toolCalls: { default: "allow", byTool: { create_return: "hook" } },
decisions: { expiresInSeconds: 3600, onExpire: "deny" }
},
limits: { maxTurns: 12 },
overrides: { model: false, reasoning: true, system: "append" }
}
})Every field but name, model and system is optional, and every policy object is
complete when empty: "policies": {} is a valid permissive policy, "compaction": {}
is a valid compaction policy. Defaults live in one place and are documented on the
page for each feature.
The fields
| Field | What it decides |
|---|---|
model | Provider name, model id, reasoning level, and a fallback ladder. |
system | The system prompt. |
tools | What it may call: MCP connections, inline servers, provider-executed tools, sandbox tools. |
toolLoading | eager / deferred / auto — see deferred tools. |
policies | The tool-call gate and what happens to an unanswered approval. |
limits | maxTurns, token budgets per session. |
compaction | When and how the transcript is compacted. |
overrides | Which axes a session may vary — see overrides. |
subagents, subagentPolicy | The sub-agent roster and its caps. |
sandbox | A machine of its own: image, resources, network, lifecycle, tool narrowing. |
identity | Whether a session must assert an identity, and which scopes. |
Versions
await client.agents.get({ id: agentId }) // the latest definition
await client.agents.versions({ id: agentId }) // the history
await client.agents.getVersion({ id: agentId, version: 3 })
// Optimistic concurrency: refuses with VersionConflict if somebody else moved it.
await client.agents.upsert({ slug, definition, expectedVersion: 7 })An agent with a sandbox has an image, and an image has to be baked. The bake is
not part of the write: upsert returns as soon as the version is stored and Plane
builds behind it. Starting a session on a version whose image is not ready is
AgentNotReady, carrying baking or failed.
let { status } = await client.agents.imageStatus({ id: agentId })
while (status === "baking") {
await new Promise((r) => setTimeout(r, 5_000))
;({ status } = await client.agents.imageStatus({ id: agentId }))
}imageStatus is a read and never starts a build, so polling it is free.
agents.rebake rebuilds the latest version's image without appending a version —
for when the image's content moves while its spec does not (a git clone step bakes
whatever main was that morning).
Reasoning
One vocabulary — none, minimal, low, medium, high, max — mapped per
adapter. A model that cannot express the level gets the option omitted, never
approximated: a thinking budget you did not ask for is a silent bill. The level
actually in force is recorded on model.request.started.
Fallback
A ladder, tried in order, and only for the failure reasons you list.
model: {
provider: "gateway",
model: "anthropic/claude-sonnet-4.6",
fallback: {
models: [{ model: "openai/gpt-5.6-luna" }, { provider: "anthropic", model: "claude-haiku-4.6" }],
// the default
on: ["unavailable", "rate_limited", "timeout", "server_error"]
}
}A rung inherits the primary's provider and reasoning level unless it states its own. The same-model retry runs inside each attempt, so a fallback happens only once backing off has given up. A failure Plane cannot classify has no reason and fails the turn rather than falling down the ladder — the list is an opt-in, and silently paying twice to hide a bug is worse than an error.
Each rung gets its own model.request.started / model.request.failed pair, so a
dead attempt's tokens never mix into the answer you render.
Promotion
Moving an agent between environments is agents.promote. It
rewrites the names the definition references and resolves every one of them in the
target, refusing as a whole if anything is missing.