Manage Agents as code
Keep your Agent definitions in your repository — reviewed, versioned, and rendered from the same data your product uses — and have the platform converge to them at deploy time.
Why manage Agents as code?
A dashboard-edited Agent is easy to start with, but its system prompt and configuration live outside your codebase: they are not code-reviewed, they can drift from the product they describe, and there is no repeatable update path across environments. Managing the definition as code fixes all three. Your CI pushes the definition; the dashboard becomes a window onto a build artifact of your repo.
The SDK verb for this is ensure. It is a convergence postcondition, not a save button: “make the platform’s definition of this Agent match this object; no-op if it already does.” Running it twice is identical to running it once.
Define the Agent
defineAgent is a pure, local constructor. It validates the definition shape and returns the canonical definition object. Because the same definition converges every environment, it must be environment-portable: account-scoped tool IDs (tool_…) are rejected — use portable builtin:, platform:, or mcp: tool references, or reference one of your saved tools by name with tool:<name> (see Reference saved tools by name).
You don’t have to write the definition by hand. To bootstrap from an Agent you already built in
the dashboard, open it in the Agent editor, press ⌘K (or Ctrl+K) to open the command
palette, and run Get code. The dialog renders your current configuration — including unsaved
edits — as a ready-to-copy defineAgent block plus the agents.ensure converge and drift-gate
snippet. If the Agent references saved tools by account-scoped ID (tool_…), the generated code
flags each one with a comment, since defineAgent rejects those — replace them with portable
builtin:, platform:, or mcp: references, or the tool:<name> form, before converging. The command is available for
standard Runtype Agents (not external A2A or Claude-managed Agents); the flow editor has the same
Get code palette command for flows.
The Agent’s identity is its name plus your account scope (organization, or personal). Which environment you converge is decided by the credentials you run with — the same ensure call with staging credentials converges staging, with production credentials production. There are no per-environment ID files and no state file.
Renaming a definition does not rename the Agent. ensure will create a new Agent under the new
name and leave the old one in place (it never deletes). Treat the name as the stable identity.
Reference saved tools by name
Built-in, platform, and MCP tools have portable IDs you can hardcode. Your saved tools (external HTTP tools, custom code tools, flow tools you built in the dashboard) get a per-account, per-environment ID (tool_…) that can’t appear in a portable definition. Reference them by name instead, with the tool:<name> form:
The text after tool: is the tool’s exact name (case-sensitive; it may contain spaces or colons). The name is the canonical reference: it’s what gets hashed, so the same definition converges to the same content hash in every environment, and it resolves to the actual tool at run time, on every dispatch — converge the tool itself before or after the Agent, recreate it, and the reference keeps working with no re-converge.
ensure validates the reference when you converge (including dryRun):
- The named tool doesn’t exist in the target environment →
ensurefails with a clear error, so the gap surfaces in CI instead of as a silent runtime drop. Create the tool first (or run your tools-as-code step before your agents). - Two saved tools share that name →
ensurefails and names both, so you can rename one. A definition that runs code shouldn’t quietly pick one of two same-named tools. (If a duplicate is created after a successful converge, dispatch keeps using the older one and logs a warning rather than failing a live conversation.)
Use tool:<name> everywhere you’d reference the tool — in toolIds, in toolConfigs /
perToolLimits keys, in approval.require, and in subagentConfig / codeModeConfig tool
pools. Raw tool_… IDs remain rejected on the ensure surface. ensure checks that the
referenced tool exists when the name appears in toolIds, subagentConfig.toolPool, or
codeModeConfig.toolPool. References in approval.require are accepted as runtime name patterns
(they may include wildcards such as mcp:*) and are not existence-checked at converge time.
When you pull an Agent that was built in the dashboard, ensure reverse-maps its saved-tool IDs to tool:<name> so the emitted definition is paste-ready. If a reference can’t be safely emitted by name — the tool was deleted, or its name is shadowed by an older same-named tool — pull leaves the raw ID and adds an entry to a warnings array explaining how to fix it. (Migrating an existing Agent from IDs to names changes its stored configuration, so the first ensure after a name-migration reports one expected updated result.)
Converge at deploy time
ensure is hash-first: in the steady state (nothing changed) it sends one tiny probe request carrying only the name and a content hash, and writes nothing. When the content differs, it writes the live Agent configuration and appends an immutable version snapshot — history is never mutated, and a dashboard edit you overwrite is still recoverable from version history.
The contentHash in every response is computed by the server over its canonical, normalized form of the definition. The SDK echoes the server’s hash on later probes, so a formatting difference in your local object can never cause an endless “changed” loop.
Publishing
By default ensure writes the live configuration and a draft version. To also re-aim the published-version pointer at the version it just created:
Detect drift in CI
A dry run is the same request with persistence switched off. It returns a coarse plan — whether anything would change, and which keys:
For a one-line PR gate, expectNoChanges throws when the plan is anything but none:
To make the apply step act only on the exact state the dry run saw (a guard against a dashboard edit landing in between), bind them with expectedRemoteHash:
Conflicts with dashboard edits
The platform records where each Agent write came from. If someone edits an ensure-managed Agent in the dashboard (or via the API or MCP), the next ensure fails with a 409 conflict by default, surfaced in the SDK as an AgentEnsureConflictError with code: 'external_modification'.
The dashboard surfaces this posture proactively. When you open an Agent that was last converged from a code definition, a Managed in code badge appears in the Agent editor header and in the edit slide-over. Clicking Save opens a confirmation dialog explaining that the next deploy may overwrite your changes; Save anyway proceeds. Saving flips the Agent’s provenance away from sdk, so the next ensure run detects the drift and either fails with a conflict (the default) or overwrites your edit (when the converge job runs with onConflict: 'overwrite'). To make a lasting change, update the Agent definition in your code instead.
You have two remediation directions:
- Repo wins. Re-run with
onConflict: 'overwrite'. The dashboard edit is overwritten in the live configuration but preserved in version history. Repo-owned Agents typically run CI with overwrite deliberately and rely on the PR-time dry-run gate as the human drift signal. - Dashboard wins. Absorb the edit into your repo with
pull, review it as a git diff, and commit or revert:
What ensure does and does not manage
ensure converges the Agent definition: name, description, icon, and the runtime configuration (model, system prompt, sampling, tools configuration, loop, error handling, and so on). It does not manage definition-adjacent state — capabilities, skill bindings, memory, conversations, or executions — and it never deletes or renames Agents. It applies to standard Runtype Agents only (not external A2A or Claude-managed Agents).
Wire protocol (direct API use)
If you are not using the TypeScript SDK, the protocol is one endpoint: POST /v1/agents/ensure.
- Probe with
{ "name": "...", "contentHash": "<sha-256>" }. A match returns{ "result": "unchanged" }; a miss returns a normal 200{ "result": "definitionRequired" }— retry with the fulldefinitionincluded. - The server recomputes the canonical hash over any submitted definition and returns it on every response. Echo the server’s hash in later probes.
- Conflicts are 409s (
external_modification,remote_changed); a submittedcontentHashthat disagrees with the server’s recompute over the definition is a 422 (content_hash_mismatch). OmitcontentHashon full requests to avoid it entirely.
GET /v1/agents/pull?name=... returns the canonical definition plus provenance (contentHash, lastModifiedSource, updatedAt, versionId).