> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.runtype.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.runtype.com/_mcp/server.

# 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](/user-guide/settings/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](#reference-saved-tools-by-name).

The following example defines a pricing assistant:

```typescript
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.',
})
```

> **Tip**
>
> 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.

> **Warning**
>
> 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>`:

```typescript
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 },
    },
  },
})
```

> **Note**
>
> 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](/developer-guides/guides/runtime-tools#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](/user-guide/tools/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:

```typescript
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](/developer-guides/integrations/mcp-servers#configure-a-runtime-server):

```typescript
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`:

```typescript
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`.

#### Python

```python
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")
```

#### Ruby

```ruby
require "runtype"

client = Runtype::Client.new(api_key: ENV.fetch("RUNTYPE_API_KEY"))

pricing_assistant = Runtype.define_agent(
  name: "Pricing Assistant",
  model: "claude-sonnet-5",
  system_prompt: "Help customers compare plans.",
  loop_config: { maxTurns: 1 }
)

result = client.ensure_agent(pricing_assistant, deploy: { alias: "live" })
pulled = client.pull_agent("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:

```typescript
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](/user-guide/products-surfaces/client-tokens-and-domain-restrictions#target-a-version-or-release-alias).

### 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:

```bash
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:

| Flag                            | What it does                                                                                                                                         | SDK 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-run`                     | Prints the plan and the changed keys without writing.                                                                                                | `dryRun: true`            |
| `--expect-no-changes`           | Plans without writing and exits with code 1 on any drift.                                                                                            | `expectNoChanges: true`   |
| `--overwrite-managed`           | Converges 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`           |
| `--json`                        | Prints 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](#bind-skills) and prints each binding that changes. To declare capabilities, add a top-level `capabilities` array. The CLI sends it as the [`capabilities` option](#declare-capabilities) 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:

```typescript
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.

```typescript
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:

```bash
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](/user-guide/agents/agent-versioning-and-publishing#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](/user-guide/agents/agent-versioning-and-publishing#per-alias-secret-bindings).

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

```bash
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](/user-guide/agents/agent-versioning-and-publishing#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](/user-guide/evals/manage-evals-as-code), or remove that step.

**`.github/workflows/agent-preview.yml`**

```yaml title=".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`.

> **Note**
>
> 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](/user-guide/agents/agent-versioning-and-publishing#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](/user-guide/agents/agent-versioning-and-publishing#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:

```bash
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](/user-guide/agents/agent-versioning-and-publishing#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:

```typescript
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`:

```typescript
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:

```bash
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`:

```typescript
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`:

```typescript
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](/user-guide/agents/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.

```typescript
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.

```typescript
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](#bind-skills) or [capabilities](#declare-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`](/user-guide/products-surfaces/client-tokens-and-domain-restrictions#create-a-token-from-code). 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:

* [Agent versioning and publishing](/user-guide/agents/agent-versioning-and-publishing): release aliases, deployment receipts, and what runs when a request names no version.
* [Manage tools as code](/user-guide/tools/manage-tools-as-code): converge the saved tools that `tool:<name>` references.
* [Manage Flows as code](/user-guide/flows/manage-flows-as-code): apply the same protocol to Flow definitions.
* [Manage evals as code](/user-guide/evals/manage-evals-as-code): converge eval suites and run them as CI gates.