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 withnpm install -g @runtypelabs/cli. - Create an API key with the
AGENTS:CREATEandAGENTS:UPDATEscopes. These scopes also let the key deploy tolive. To deploy to a preview alias, addAGENTS:DEPLOY:PREVIEW. To pull a definition, addAGENTS: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
liveonly.
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:
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>:
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:
ensurefails with an error. Create the tool before the Agent deployment step so that CI reports the missing dependency. - A duplicate name:
ensurefails 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’sconfig.agentId.flow:<name>: the flow-as-tool runtime tool’sconfig.flowId.
The following example references a saved subagent and a saved flow:
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:
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:
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
Ruby
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:
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, andversionNumber: the alias and the version that it now points at.revision: the alias revision. Pass it asIf-Matchthe 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:falsewhen 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:
The following table lists the flags that runtype agents ensure accepts:
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:
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.
The following command runs the same converge from the CLI:
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
unchangedand moves nothing. - A preview deploy rejects
onConflictandexpectedRemoteHashwith400. Both options guard writes to the saved configuration, and a preview deploy makes none. Remove them, or deploy tolive. - 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
livepoints at nothing. - Preview aliases need an organization. A personal-account API key gets
400withcode: "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:
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.
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 asagent:NAME, and Flows asflow: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:
Replace the following:
YOUR_SUITE_ID: an eval suite in the target organizationPricing Assistant: the Agent name, which identifies the Agent in both organizations
Each step does the following:
preparepulls the definition from the source organization and stages it at a preview alias in the target. It doesn’t change the target’slive. It writespromotion.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.validatechecks the staged candidate in the target organization and reports every named reference that it can’t resolve there, such as atool:NAMEthat exists in staging and not in production. It exits with a nonzero code when any reference is unresolved.evaluateruns an eval suite in the target organization against the staged version, and requires--suite. It exits with code 1 when the suite fails.activatedoesn’t check the result, so stop the job on a failedevaluateor read the result before you activate.activatedeploys the staged version at the target alias and records the source IDs, content hashes, and commit on the deployment receipt.--aliasdefaults tolive.--idempotency-keydefaults to a key derived from the manifest, so a retried job doesn’t deploy twice.
Every step also accepts these options:
--source-key-envand--target-key-env: the environment variables that hold the API keys. The defaults areRUNTYPE_SOURCE_API_KEYandRUNTYPE_TARGET_API_KEY.--source-urland--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
422withcode: "selector_unsupported_agent_type", and they keep running their saved configuration. - Personal-account Agents have no preview aliases. A preview deploy returns
400withcode: "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
livealias. 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:
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:
The SDK throws AgentDriftError when the plan reports create or update. From the CLI, --expect-no-changes exits with code 1 on any drift:
To bind an apply step to the exact state that the dry run inspected, pass its remoteHash as expectedRemoteHash:
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-managedfrom 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:
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.
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.
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:
- Send a hash-only request with
nameandcontentHash. A matching hash returns{ "result": "unchanged" }. A nonmatching hash returns HTTP status code200with{ "result": "definitionRequired" }. Resend the request with the fulldefinition. - For a full request, let the server recompute the hash. The response includes
contentHash. Use it in later hash-only requests. - Handle conflicts as follows. The API returns status code
409forexternal_modificationorremote_changed. If the submittedcontentHashdoesn’t match the server’s hash for the definition, the API returns422withcontent_hash_mismatch. OmitcontentHashon full requests to avoid this mismatch.
The API also rejects these requests:
- A request with neither
definitionnorcontentHashreturns400. - A
definition.namethat differs from the top-levelnamereturns400. - A preview deploy that can’t be applied returns
500withcode: "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: release aliases, deployment receipts, and what runs when a request names no version.
- Manage tools as code: converge the saved tools that
tool:<name>references. - Manage Flows as code: apply the same protocol to Flow definitions.
- Manage evals as code: converge eval suites and run them as CI gates.