Skip to navigation

Manage Agents as code

Keep your Agent definitions in your repository. Review and version them with your code, then converge them at deploy time. The dashboard displays the definition that your deployment writes.

Why manage Agents as code?

When you edit an Agent in the dashboard, its system prompt and configuration live outside your codebase. You can’t review those changes with your code, and definitions can drift across environments. Managing Agents as code gives CI a repeatable update path: CI converges the definition, and the dashboard displays the deployed result.

The SDK method ensure is a convergence operation, not a save operation. It makes the platform definition match the object you provide and does nothing when they already match. After the definition matches, repeating the same call makes no content changes.

Before you begin

Set up the following before you converge an Agent:

  • Install the TypeScript SDK with npm install @runtypelabs/sdk. To converge from the command line or CI, also install the CLI with npm install -g @runtypelabs/cli.
  • Create an API key with the AGENTS:CREATE and AGENTS:UPDATE scopes. These scopes also let the key deploy to live. To deploy to a preview alias, add AGENTS:DEPLOY:PREVIEW. To pull a definition, add AGENTS:READ. For more information, see Managing API keys.
  • To use preview aliases or promote between organizations, converge into an organization account. A personal account can converge and deploy to live only.

Define the Agent

defineAgent is a local constructor that doesn’t make network requests. It validates the definition shape and returns the canonical definition object. To keep the definition portable across environments, use builtin:, platform:, or mcp: references, or reference a saved resource by name with tool:<name>, agent:<name>, or flow:<name>. The constructor rejects account-scoped IDs that start with tool_, agent_, or flow_. For more information, see Reference saved tools by name.

The following example defines a pricing assistant:

import { defineAgent } from '@runtypelabs/sdk'
export const pricingAssistant = defineAgent({
name: 'Pricing Assistant',
description: 'Marketing pricing page chat assistant.',
icon: '💬',
model: 'claude-sonnet-5',
temperature: 0.7,
loopConfig: { maxTurns: 1 },
systemPrompt: 'Help customers compare plans.',
})

To bootstrap a definition from an Agent that you built in the dashboard, open the Agent editor. Press ⌘K on macOS or Ctrl+K on other platforms to open the command palette, then run Get code. The dialog includes your current configuration, including unsaved edits, and generates a defineAgent block with agents.ensure and drift-check examples.

If the Agent references a saved tool by an account-scoped ID that starts with tool_, the generated code marks each reference with a comment. Replace each one with a portable reference or a tool:<name> reference before you converge. Get code supports standard Runtype Agents, not external A2A (Agent-to-Agent) or Claude Managed Agents. The flow editor provides the same command for flows.

An Agent’s identity is its name and your account scope, which is organization or personal. The API key decides which account ensure writes to. To keep staging and production separate, use one organization for each and give CI an API key for each organization. You don’t need per-environment ID files or a state file.

Renaming a definition doesn’t rename the Agent. ensure creates an Agent under the new name and leaves the old Agent in place because it never deletes. Treat the name as the stable identity.

Reference saved tools by name

Built-in, platform, and Model Context Protocol (MCP) tools have portable IDs that you can hardcode. Saved tools receive a per-account ID with the tool_ prefix. This includes external HTTP tools, custom code tools, and flow tools that you build in the dashboard. Don’t include those IDs in a portable definition. Reference a saved tool by its exact name with tool:<name>:

export const supportAgent = defineAgent({
name: 'Support Agent',
model: 'claude-sonnet-5',
tools: {
toolIds: ['builtin:web_search', 'tool:My Scraper'],
toolConfigs: { 'tool:My Scraper': { depth: 2 } },
approval: {
require: ['tool:My Scraper'],
choices: { alwaysAllow: true },
},
},
})

Set approval.choices.alwaysAllow to true to show an Always allow button in the approval prompt. Runtype stores a persistent grant when an end user selects it, so future runs of that Agent skip the prompt for that tool.

View or revoke grants in the Agent editor under Safety, or use GET /v1/tool-approval-grants and DELETE /v1/tool-approval-grants/{id}. For more information, see Tool approval grants.

tool:<name> uses the tool’s exact, case-sensitive name, including spaces or colons. The same name and definition produce the same content hash in each environment. Runtype resolves the name to the saved tool each time the Agent runs.

Create or converge the tool before you converge the Agent. After that, you can recreate the tool without running ensure again, because the reference resolves at run time. To converge saved tools from code, see Manage tools as code.

ensure, including a dry run, validates each named reference:

  • A missing name: ensure fails with an error. Create the tool before the Agent deployment step so that CI reports the missing dependency.
  • A duplicate name: ensure fails and identifies both tools. Rename one tool before you converge the Agent.
  • A duplicate created after a successful converge: the Agent keeps using the older tool and logs a warning instead of failing a live conversation.

Use tool:<name> anywhere you reference the tool: in toolIds, the keys of toolConfigs and perToolLimits, approval.require, and the subagentConfig and codeModeConfig tool pools. ensure checks that the tool exists when the name appears in toolIds, subagentConfig.toolPool, or codeModeConfig.toolPool. It treats approval.require entries as name patterns, so it accepts wildcards such as mcp:* without checking that a tool exists.

When you pull an Agent built in the dashboard, pull replaces saved-tool IDs with tool:<name>. You can paste the returned definition into your code. If pull can’t safely emit a name, it leaves the raw ID and adds a warnings entry. This happens when the tool was deleted or when an older tool has the same name. The warning explains the fix.

Migrating an existing Agent from IDs to names changes its stored configuration, so the first ensure after the migration returns updated.

Saved subagents and flow tools

When an Agent delegates to a saved subagent or calls a saved flow as a tool, reference those resources by name:

  • agent:<name>: the saved subagent’s config.agentId.
  • flow:<name>: the flow-as-tool runtime tool’s config.flowId.

The following example references a saved subagent and a saved flow:

export const routingAgent = defineAgent({
name: 'Routing Agent',
model: 'claude-sonnet-5',
tools: {
runtimeTools: [
{
toolType: 'subagent',
name: 'escalate',
description: 'Escalate to a specialist',
parametersSchema: {},
config: { agentId: 'agent:Billing Specialist' },
},
{
toolType: 'flow',
name: 'lookup',
description: 'Look up an order',
parametersSchema: {},
config: { flowId: 'flow:Order Lookup' },
},
],
},
})

ensure checks that each referenced Agent or Flow exists and that exactly one has that name. pull maps agent_ and flow_ IDs to the name forms. An inline subagent or inline flow embeds its definition, so it needs no reference.

Connect inline MCP servers

To give an Agent the tools of a remote MCP server without saving the server first, list the server in tools.mcpServers. Each entry needs an id and a url. The SDK types the list as CustomMCPServer[], and the fields match runtime MCP servers:

import { defineAgent } from '@runtypelabs/sdk'
export const docsAgent = defineAgent({
name: 'Docs Assistant',
model: 'claude-sonnet-5',
systemPrompt: 'Answer questions about public repositories.',
tools: {
mcpServers: [
{
id: 'deepwiki',
name: 'DeepWiki',
url: 'https://mcp.deepwiki.com/mcp',
auth: { type: 'none' },
},
],
},
})

ensure stores the entries as you send them and includes them in the content hash, so an unchanged list reports unchanged. pull returns the entries unchanged, including any credential fields. Don’t put a literal token in auth. Use a {{secret:NAME}} reference, such as auth: { type: 'bearer', token: '{{secret:NOTION_TOKEN}}' }, so that the definition in your repository and the stored configuration hold only the reference.

Converge at deploy time

To converge the definition, configure the SDK with an API key and call Runtype.agents.ensure:

import { Runtype } from '@runtypelabs/sdk'
Runtype.configure({ apiKey: process.env.RUNTYPE_API_KEY })
const result = await Runtype.agents.ensure(pricingAssistant)

The result includes result, agentId, versionId, and contentHash. The result value is unchanged, created, or updated.

When the definition already matches, ensure writes nothing. When it differs, ensure updates the Agent’s saved configuration and adds an immutable version to its version history. A dashboard edit that you overwrite stays recoverable in version history.

Runtype computes contentHash from the normalized definition, so formatting differences in your local object don’t cause repeated updates.

Converge from Python or Ruby

The Python (runtype-sdk) and Ruby (runtype) SDKs provide the same ensure and pull operations. Build the definition with define_agent, which accepts snake_case or camelCase top-level fields. The options match the TypeScript ones in snake_case: dry_run, expect_no_changes, on_conflict, deploy, release, expected_remote_hash, version, skills, and capabilities.

import os
from runtype import RuntypeClient, define_agent
client = RuntypeClient(api_key=os.environ["RUNTYPE_API_KEY"])
pricing_assistant = define_agent(
name="Pricing Assistant",
model="claude-sonnet-5",
system_prompt="Help customers compare plans.",
loop_config={"maxTurns": 1},
)
result = client.agents.ensure(pricing_assistant, deploy={"alias": "live"})
pulled = client.agents.pull("Pricing Assistant")

Deploy the converged version

By default, ensure writes the saved configuration and a draft version, and moves no release alias. To deploy the version that ensure creates, name the alias to activate:

await Runtype.agents.ensure(pricingAssistant, { deploy: { alias: 'live' } })

release: 'publish' is an older spelling of deploy: { alias: 'live' }, and release: 'none' is the same as omitting both. Both keep working. The SDK throws an error if you pass both release and deploy, and the API rejects the same combination with 400.

The response includes a deployment object with these fields:

  • alias, versionId, and versionNumber: the alias and the version that it now points at.
  • revision: the alias revision. Pass it as If-Match the next time you move the alias.
  • receiptId: the deployment receipt that this converge added to the Agent’s deployment history. It’s present only when the converge moved the alias.
  • changed: false when the alias already pointed at this version.

To serve the Agent in a chat widget, create a client token for it with Runtype.clientTokens.ensure. A client token without a target runs the version at live when your organization’s live-default setting is on, so deploy with deploy: { alias: 'live' } when the widget must pick up each converge. For details, see Client tokens and domain restrictions.

Converge from the CLI

runtype agents ensure <file> reads a JSON definition from a file, or from standard input when <file> is -. It doesn’t load TypeScript or JavaScript modules. The JSON is the object that defineAgent returns, so you can serialize a defineAgent module and pipe it to the CLI:

npx tsx -e "
import { pricingAssistant } from './agents/pricing-assistant.ts'
process.stdout.write(JSON.stringify(pricingAssistant))
" | runtype agents ensure - --deploy live

The following table lists the flags that runtype agents ensure accepts:

FlagWhat it doesSDK equivalent
--deploy <alias>Deploys the converged version at this alias.deploy: { alias }
--release <mode>Older spelling of --deploy: none or publish. Can’t be combined with --deploy.release
--dry-runPrints the plan and the changed keys without writing.dryRun: true
--expect-no-changesPlans without writing and exits with code 1 on any drift.expectNoChanges: true
--overwrite-managedConverges even when the last edit came from the dashboard.onConflict: 'overwrite'
--expected-remote-hash <hash>Applies only while the live Agent still has the remoteHash that a dry run printed. Only for a converge without --deploy or with --deploy live.expectedRemoteHash
--bindings-file <file>Reads a JSON object of { SECRET_NAME: value } and binds it to the --deploy alias. Requires --deploy.deploy.bindings
--label <text>Sets the version label.version.label
--notes <text>Sets the version notes.version.notes
--jsonPrints the result as JSON.None

To bind skills from the CLI, add a top-level skills array to the JSON file. The CLI sends it as the skills option and prints each binding that changes. To declare capabilities, add a top-level capabilities array. The CLI sends it as the capabilities option and prints each capability that changes. --expect-no-changes treats a skill or capability change as drift.

To pull a definition, run runtype agents pull <name>. It prints the JSON file that runtype agents ensure reads, with the skill bindings as a top-level skills array. Pass -o <file> to write the file, or --json to print the full pull response.

Label the version

Pass an optional version object to record provenance on the version that ensure adds, so version history shows which commit produced it:

await Runtype.agents.ensure(pricingAssistant, {
version: { label: process.env.GIT_SHA, notes: 'Promoted from CI run 4821.' },
})

Both fields are optional. label holds up to 100 characters, typically a git SHA or a release tag, and notes holds up to 2000. Omit version to keep the default sdk-ensure label and the generated notes.

Deploy a preview

To deploy a preview, name any alias other than live. The converge saves a candidate version and points that alias at it. It changes nothing else: the Agent’s saved configuration stays as it was, and live keeps serving the same version.

const result = await Runtype.agents.ensure(pricingAssistant, {
deploy: { alias: `pr-${process.env.PR_NUMBER}` },
version: { label: process.env.GIT_SHA },
})
console.log(result.deployment?.alias, result.deployment?.versionId)

The following command runs the same converge from the CLI:

runtype agents ensure pricing-assistant.json --deploy pr-482 --label "$GITHUB_SHA"

Alias names use lowercase letters, digits, and hyphens, start with a letter or digit, and have at most 63 characters. latest, publish, and none are reserved. A name derived from a branch, such as feature/Pricing, fails with 400. For more information, see Release aliases.

Before you add previews to a pipeline, review these behaviors:

  • A preview converge compares against the preview. It compares the definition with the version that this alias was last deployed from, not with the saved configuration. A preview that already matches reports unchanged and moves nothing.
  • A preview deploy rejects onConflict and expectedRemoteHash with 400. Both options guard writes to the saved configuration, and a preview deploy makes none. Remove them, or deploy to live.
  • An unchanged converge still renews the preview. The 14-day expiry restarts on every successful converge, including one that changed nothing.
  • A preview deploy of an unknown Agent creates the Agent. The preview alias points at the candidate, and live points at nothing.
  • Preview aliases need an organization. A personal-account API key gets 400 with code: "alias_requires_organization".
  • Previews have no count limit. Archive a preview when its pull request closes. A preview that nothing renews expires after 14 days.

To make a preview’s tools call that pull request’s own environment while the version content stays identical to what live runs, pass deploy.bindings. For more information, see Per-alias secret bindings.

To test the preview, run it by alias from the CLI:

runtype agents execute YOUR_AGENT_ID --alias pr-482 -m "Which plan includes SSO?"

Replace YOUR_AGENT_ID with the agentId that ensure returned. In the dashboard, open the Agent, select the Deployments tab, and click Test this version on the preview alias. Compare to live shows the difference from the version that live serves. For more information, see Run a specific version.

A pull-request workflow

The following GitHub Actions workflow gives each pull request its own preview of the Agent. The preview job converges a preview and grades it. The cleanup job archives the preview alias when the pull request closes.

The grading step needs at least one *.eval.ts suite in the repository that targets this Agent. runtype eval run discovers suites from those files, and --alias chooses which version they run against. To write one, see Manage evals as code, or remove that step.

.github/workflows/agent-preview.yml
name: Agent preview
on:
pull_request:
types: [opened, synchronize, closed]
jobs:
preview:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
env:
RUNTYPE_API_KEY: ${{ secrets.RUNTYPE_PREVIEW_API_KEY }}
PREVIEW_ALIAS: pr-${{ github.event.number }}
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm install -g @runtypelabs/cli
- run: |
npx tsx -e "
import { pricingAssistant } from './agents/pricing-assistant.ts'
process.stdout.write(JSON.stringify(pricingAssistant))
" > pricing-assistant.json
- run: |
runtype agents ensure pricing-assistant.json \
--deploy "$PREVIEW_ALIAS" \
--label "${{ github.sha }}"
- run: runtype eval run --alias "$PREVIEW_ALIAS"
cleanup:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
env:
RUNTYPE_API_KEY: ${{ secrets.RUNTYPE_PREVIEW_API_KEY }}
steps:
- run: npm install -g @runtypelabs/cli
- run: runtype agents aliases archive "pr-${{ github.event.number }}" --all

Give the workflow’s API key the AGENTS:CREATE, AGENTS:UPDATE, AGENTS:DEPLOY:PREVIEW, and EVALS:WRITE scopes. Leaving out AGENTS:DEPLOY:LIVE doesn’t stop this key from deploying to live: AGENTS:CREATE and AGENTS:UPDATE also permit live deploys, for compatibility with API keys created before release aliases. Store the key as a secret that only this workflow reads, and review changes to the workflow’s --deploy value.

agents aliases archive ALIAS --all archives that alias name on every Agent in the organization. Use it when one branch converged several Agents. To see what it would archive first, add --dry-run. To archive one alias on one Agent, pass both: runtype agents aliases archive AGENT_ID ALIAS.

Archiving isn’t a delete. The alias keeps its deployment history, runs that already started finish, and the memory that the preview wrote stays where it is. To keep preview memory out of production, see the namespace ladder.

Per-environment values

Keep the definition byte-identical across environments and let the values vary underneath it:

  • Reference credentials and environment-specific endpoints as {{secret:NAME}} and resolve them from the organization’s secrets. The definition then carries the name, not the value, and its content hash is the same everywhere.
  • Reference saved tools as tool:NAME, saved Agents as agent:NAME, and Flows as flow:NAME, so each organization resolves its own resource at run time.
  • When one preview needs a different value for the same name, bind it to the alias instead of editing the definition. For more information, see Per-alias secret bindings.

If you change a value inside the definition, such as an inline URL or a different model, each environment runs a different definition. An eval result from staging then doesn’t apply to production.

Promote across organizations

To keep staging and production fully separate, use separate organizations with separate API keys and separate data. To move a definition from one organization to the other, run the four promotion steps. The steps read the source and target API keys from environment variables, so no key appears on the command line. prepare and validate read both keys, and evaluate and activate read only the target key. In CI, set RUNTYPE_SOURCE_API_KEY and RUNTYPE_TARGET_API_KEY as secrets.

The following commands promote the Pricing Assistant from the source organization to live in the target organization:

runtype agents promote prepare "Pricing Assistant" --alias candidate --commit "$GITHUB_SHA"
runtype agents promote validate promotion.json
runtype agents promote evaluate promotion.json --suite YOUR_SUITE_ID
runtype agents promote activate promotion.json --alias live --reason "Release 2026.09"

Replace the following:

  • YOUR_SUITE_ID: an eval suite in the target organization
  • Pricing Assistant: the Agent name, which identifies the Agent in both organizations

Each step does the following:

  • prepare pulls the definition from the source organization and stages it at a preview alias in the target. It doesn’t change the target’s live. It writes promotion.json, which records the Agent name, the source Agent ID, version ID, content hash, and commit, and the target Agent ID, staged version ID, alias, and alias revision. To write it somewhere else, pass --manifest.
  • validate checks the staged candidate in the target organization and reports every named reference that it can’t resolve there, such as a tool:NAME that exists in staging and not in production. It exits with a nonzero code when any reference is unresolved.
  • evaluate runs an eval suite in the target organization against the staged version, and requires --suite. It exits with code 1 when the suite fails. activate doesn’t check the result, so stop the job on a failed evaluate or read the result before you activate.
  • activate deploys the staged version at the target alias and records the source IDs, content hashes, and commit on the deployment receipt. --alias defaults to live. --idempotency-key defaults to a key derived from the manifest, so a retried job doesn’t deploy twice.

Every step also accepts these options:

  • --source-key-env and --target-key-env: the environment variables that hold the API keys. The defaults are RUNTYPE_SOURCE_API_KEY and RUNTYPE_TARGET_API_KEY.
  • --source-url and --target-url: the API base URL for each organization.

The --commit option of prepare defaults to the RUNTYPE_COMMIT or GITHUB_SHA environment variable.

API keys never cross organizations. The manifest holds no secret values, and the target organization’s own API key decides what the activation can do.

Limitations

Review these limits before you build a release process on the promotion steps:

  • Flows have no aliases. Only Agents have release aliases. An Agent that calls a Flow by name runs whichever Flow has that name at run time.
  • External and Claude Managed Agents accept no version selector. They return 422 with code: "selector_unsupported_agent_type", and they keep running their saved configuration.
  • Personal-account Agents have no preview aliases. A preview deploy returns 400 with code: "alias_requires_organization".
  • Previews are configuration previews, not sandboxes. A preview shares the organization’s connections, Records, and secrets with live. Per-alias bindings override individual secret values and isolate nothing else.
  • A version pins the Agent’s own definition, not its dependencies. Sub-Agents, Flows, saved tools, MCP servers, Skills, and secrets referenced by name resolve at run time. A deployment receipt records their fingerprints at activation. A matching fingerprint shows that a definition is unchanged, not that its behavior is.
  • A sub-Agent runs the version at its own live alias. A parent version doesn’t pin the versions of the Agents it delegates to, whether it reaches them through a capability, a Skill, or a nested reference.
  • Rollback restores configuration only. It doesn’t undo external side effects, data already written, or later edits to the resources that the version references by name.

To roll live back after a bad converge, run runtype agents aliases rollback AGENT_ID live --revision REVISION. Replace AGENT_ID with the Agent’s ID and REVISION with the alias revision that you last read. For more information, see Deployment history.

Detect drift in CI

A dry run sends the full definition without saving changes. It returns a plan that reports whether the definition would change and which keys would change:

const plan = await Runtype.agents.ensure(pricingAssistant, { dryRun: true })

The plan includes result: 'plan', a changes value of none, create, or update, changedKeys, contentHash, and an optional remoteHash.

To make a drift check fail when the plan isn’t none, pass expectNoChanges: true:

await Runtype.agents.ensure(pricingAssistant, { expectNoChanges: true })

The SDK throws AgentDriftError when the plan reports create or update. From the CLI, --expect-no-changes exits with code 1 on any drift:

runtype agents ensure pricing-assistant.json --expect-no-changes

To bind an apply step to the exact state that the dry run inspected, pass its remoteHash as expectedRemoteHash:

await Runtype.agents.ensure(pricingAssistant, { expectedRemoteHash: plan.remoteHash })

If the Agent changed after the dry run, ensure fails with AgentEnsureConflictError and code: 'remote_changed'. Run the dry run again and review the new plan.

Resolve conflicts with dashboard edits

Runtype records where each Agent edit came from. If someone edits an Agent managed by ensure in the dashboard, through the API, or through MCP, the next ensure returns status code 409 by default. The SDK throws this as an AgentEnsureConflictError with code: 'external_modification'. The error’s lastModifiedSource, modifiedAt, and currentHash properties describe the edit, so a CI script can report it.

When you open an Agent managed by ensure, the dashboard shows a Managed in code badge. The badge appears in the Agent editor header, the edit slide-over, and next to the Agent’s name on the Agents list page. To find every Agent managed by code, open Agents and filter by Managed in code. Clicking Save, or publishing a version from Version History, opens a confirmation dialog. Click Save anyway to continue.

Saving in the dashboard marks the Agent as edited outside code, so the next ensure detects the drift and fails with a conflict.

You can resolve the conflict in either of these ways:

  • Repository wins: pass onConflict: 'overwrite', or --overwrite-managed from the CLI. The Agent’s saved configuration uses the repository definition, and the dashboard edit stays in version history. In CI, pair this option with a dry-run check to surface drift.
  • Dashboard wins: pull the definition, review it as a git diff, then commit or revert the change.

To pull the definition by name, call Runtype.agents.pull:

const { definition, contentHash, lastModifiedSource, updatedAt } =
await Runtype.agents.pull('Pricing Assistant')

pull returns 404 when no Agent has that name in your account.

The pulled contentHash covers the definition and the Agent’s capabilities. Pass it as expectedRemoteHash to make a later ensure fail with 409 and remote_changed if either one changes after the pull. ensure accepts it whether or not you declare capabilities.

Bind skills

To bind Agent Skills to the Agent, pass the skills option. Each entry names a skill by its slug. To pin the binding to one version of the skill, set version to that version number. Omit version to follow the skill’s published version.

await Runtype.agents.ensure(pricingAssistant, {
deploy: { alias: 'live' },
skills: [
{ name: 'refund-policy', version: 3 },
{ name: 'tone-guide' },
{ name: 'beta-offers', enabled: false },
],
})

The list is the complete set of skills for the Agent. ensure binds each listed skill, updates the version pin and the enabled flag of each existing binding, and unbinds each skill that isn’t listed. The Agent loads the skills in list order. To leave the bindings unchanged, omit skills. To unbind every skill, pass an empty list.

Create each skill before you bind it, for example with Runtype.skills.ensure. ensure returns 400 and writes nothing when a skill doesn’t exist in your account, when a pinned version doesn’t exist, or when the list names a skill twice. Changing a binding requires an API key with skill write scope (SKILLS:WRITE).

The response includes a skillBindings array. Each entry has the skill name, the action (bind, update, unbind, or unchanged), the pinned version (null when the binding follows the published version), and enabled. A dry run reports the same array without writing, and expectNoChanges treats any binding change as drift. pull returns the Agent’s current bindings as skills, in the shape that the option accepts.

Skill bindings belong to the Agent, not to a release alias, so a binding change takes effect immediately, whether or not the converge deploys. For the same reason, ensure rejects skills together with a preview deploy (deploy with an alias other than live).

Declare capabilities

To attach flows, other Agents, and saved tools to the Agent as capabilities, pass the capabilities option. Each entry sets the toolName that the Agent calls and names exactly one source by name: flow, agent, or tool. Because sources are named, not identified by ID, the same declaration works in every account.

await Runtype.agents.ensure(pricingAssistant, {
deploy: { alias: 'live' },
capabilities: [
{ toolName: 'lookup_order', flow: 'Order Lookup', description: 'Find an order by ID' },
{ toolName: 'ask_researcher', agent: 'Researcher' },
{ toolName: 'get_weather', tool: 'weather', enabled: false },
],
})

Each entry also accepts description, parametersSchema, and enabled. If you omit parametersSchema, Runtype derives it from the flow’s inputs or the saved tool’s own schema.

The list is the complete set of capabilities for the Agent, in display order. ensure adds each listed capability, updates each existing capability that differs, and removes each capability that isn’t listed. A partial list therefore removes the capabilities you leave out. To leave the capabilities unchanged, omit capabilities. To remove every capability, pass an empty list.

When you pass capabilities, they are part of the content hash. A change to the capabilities alone updates the Agent and appends a version that carries them. The response includes a capabilityChanges array. Each entry has the toolName, the action (add, update, remove, or unchanged), the source kind and name, and enabled. A dry run reports the same array without writing and lists capabilities in changedKeys.

If someone changes the capabilities in the dashboard or through the capability API after ensure last converged them, ensure returns 409 with the code external_modification instead of replacing them. Pass onConflict: 'overwrite' to replace them with the declared list, or pull the Agent to absorb the edit.

ensure returns 400 and writes nothing when a source doesn’t exist in your account, is inactive, or shares its name with another source of the same kind. Capabilities belong to the Agent, not to a release alias, so ensure rejects capabilities together with a preview deploy (deploy with an alias other than live). pull returns the Agent’s current capabilities as capabilities, in the shape that the option accepts, and warns about any capability that ensure would refuse.

What ensure does and doesn’t manage

ensure converges the Agent definition: its name, description, icon, and runtime configuration. This includes the model, system prompt, sampling, tool configuration, loop settings, and error handling. When you pass skills or capabilities, it also converges the Agent’s skill bindings or capabilities. It doesn’t manage related state such as client tokens, memory, conversations, or executions. To manage a client token as code, use Runtype.clientTokens.ensure. It never deletes or renames Agents. ensure applies only to standard Runtype Agents, not external A2A (Agent-to-Agent) or Claude Managed Agents.

Call the API directly

If you call the API directly, use POST /v1/agents/ensure. The protocol has three steps:

  1. Send a hash-only request with name and contentHash. A matching hash returns { "result": "unchanged" }. A nonmatching hash returns HTTP status code 200 with { "result": "definitionRequired" }. Resend the request with the full definition.
  2. For a full request, let the server recompute the hash. The response includes contentHash. Use it in later hash-only requests.
  3. Handle conflicts as follows. The API returns status code 409 for external_modification or remote_changed. If the submitted contentHash doesn’t match the server’s hash for the definition, the API returns 422 with content_hash_mismatch. Omit contentHash on full requests to avoid this mismatch.

The API also rejects these requests:

  • A request with neither definition nor contentHash returns 400.
  • A definition.name that differs from the top-level name returns 400.
  • A preview deploy that can’t be applied returns 500 with code: "alias_activation_failed". Nothing is written, so you can retry the request.

Every full request also accepts an optional version object, { "label": "a1b2c3d", "notes": "Promoted from CI run 4821." }, recorded on the version that the request adds. Both fields are optional. label holds up to 100 characters and notes holds up to 2000. The server trims both and treats a blank value as omitted. Omitting version keeps the default sdk-ensure label and the generated notes.

A full request also accepts deploy, { "alias": "pr-482" }, which deploys the version that this converge adds at that release alias and returns a deployment object. release remains accepted as the older spelling. Sending both returns 400.

To pull an Agent definition directly, send GET /v1/agents/pull with the name query parameter. The API returns the canonical definition and these fields: contentHash, lastModifiedSource, updatedAt, and versionId.

Next steps

Continue with these guides: