Skip to navigation

Manage tools as code

Keep your Tool definitions in your repository, review them with your code, and apply them at deploy time with ensure. ensure compares your definition with the saved Tool and creates or updates the Tool only when they differ. For related guides, see Manage Agents as code and Manage Flows as code.

To run ensure, including a dry run, your API key needs the Tools (Write) or Tools (Full) permission. To run pull, it needs Tools (Read) or Tools (Full). Tools (Write) alone doesn’t allow pull. Without the required permission, the API returns status code 403. To change a key’s permissions, see Managing API keys.

Define the tool

defineTool is a local constructor that doesn’t make network requests. It checks the top-level definition shape and returns a canonical definition object with the Tool’s name, description, toolType, parametersSchema, and config fields.

Use the following code to define a weather lookup Tool:

TypeScript SDK
import { defineTool } from '@runtypelabs/sdk'
export const weatherLookup = defineTool({
name: 'Weather Lookup',
description: 'Fetch the current weather for a city',
toolType: 'external',
parametersSchema: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
config: { url: 'https://example.com/weather?q={{city}}', method: 'GET' },
})

Set toolType to one of external, custom, graphql, mcp, local, flow, or subagent. For credentials in an external Tool’s config, use a secret reference such as {{secret:API_KEY}}, where API_KEY is the secret name. Other Tool types can’t reference secrets. A definition with secret references also needs the Secrets (Read) or Secrets (Full) permission on your API key. See Managing secrets.

ensure matches a Tool by its name within your organization or personal account. The API key you configure selects the account and environment. If you rename a definition, ensure creates a new Tool and leaves the old one in place, because ensure never deletes or renames Tools. If several Tools share a name, ensure and pull act on the oldest one, so rename or delete duplicates before you manage that name as code.

flow and subagent Tools reference saved Flows and Agents through config.flowId and config.agentId. Raw flow_ and agent_ IDs belong to one account and environment. For portable definitions, use name references such as flow:Order Lookup and agent:Billing Specialist. Runtype resolves these by exact, case-sensitive name each time the Tool runs, not during ensure, so create the Flow or Agent in each environment before the Tool runs. If several share a name, the oldest one is used.

Apply the definition at deploy time

Configure the SDK and apply the Tool definition with the following code:

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

The response includes result, toolId, and contentHash. The result value is unchanged, created, or updated. When the saved Tool already matches and is managed in code, ensure sends only a content hash and writes nothing. Otherwise it sends the full definition and creates or updates the Tool, unless the Tool was edited outside code. For that case, see Resolve conflicts with dashboard edits.

The content hash covers toolType, description, parametersSchema, and config. The name isn’t part of the hash.

A created Tool is active immediately. An update takes effect right away for every Agent and Flow step that uses the Tool. Tools don’t have versions, so there’s no draft or publish step, no release: 'publish' option, and no versionId in the result. To stage a change, apply it in a separate environment or account first.

ensure doesn’t change which Agents or Flow steps use the Tool.

Troubleshoot validation errors

The API returns status code 400 when the definition fails validation for any of the following reasons:

  • name is longer than 100 characters.
  • description is longer than 500 characters.
  • The definition has a field other than name, description, toolType, parametersSchema, or config. With the SDK, defineTool rejects an unknown field before any request is sent.
  • The code in a custom Tool fails validation. The response includes errors and warnings arrays. See Creating custom tools.
  • A {{secret:...}} reference is mistyped, names a secret that isn’t configured or was revoked, or appears in a Tool type other than external.

Adopt an existing Tool

To bring a Tool that you created in the dashboard under code management, pull its definition first:

  1. Call Runtype.tools.pull('Weather Lookup') and commit the returned definition to your repository.
  2. Run ensure. Because the content matches, ensure returns unchanged and marks the Tool as managed in code.

If you skip the pull and your definition differs from a Tool that was last saved outside code, the first ensure fails with a conflict. See Resolve conflicts with dashboard edits.

Detect drift in CI

A dry run sends the full definition, writes nothing, and returns a plan. Run a dry run with the following code:

TypeScript SDK
const plan = await Runtype.tools.ensure(weatherLookup, { dryRun: true })

The plan includes result: 'plan', a changes value of none, create, or update, changedKeys, contentHash, and an optional remoteHash. The changedKeys array names the changed fields, such as description, parametersSchema, or config.url for a changed key within config.

To fail a CI check when the plan isn’t none, pass expectNoChanges: true. This option runs a dry run and never writes:

TypeScript SDK
import { ToolDriftError } from '@runtypelabs/sdk'
try {
await Runtype.tools.ensure(weatherLookup, { expectNoChanges: true })
} catch (err) {
if (err instanceof ToolDriftError) console.error(err.plan.changedKeys)
throw err
}

A dry run compares content only, so it never reports an external_modification conflict. A Tool edited in the dashboard shows as update, the same as a change in your code. To tell the two apart, call pull and check lastModifiedSource. The values sdk and terraform mean the Tool is managed in code. A value such as dashboard, api, or mcp means someone edited it outside code, and a later apply fails with a conflict.

Apply a reviewed plan

To make sure the apply step writes over the same state that the dry run inspected, pass the plan’s remoteHash as expectedRemoteHash:

TypeScript SDK
await Runtype.tools.ensure(weatherLookup, { expectedRemoteHash: plan.remoteHash })

If the saved Tool changed after the dry run, the SDK throws a ToolEnsureConflictError with code: 'remote_changed'. Run the dry run again and review the new plan.

Resolve conflicts with dashboard edits

Runtype records where each Tool write came from. Writes from ensure (sdk) and from the Terraform provider (terraform) count as managed in code and don’t conflict with each other. The dashboard shows a Managed in code badge on these Tools and warns you before you save an edit.

When an edit outside code, from the dashboard, the API, or the MCP server, changes a managed Tool so that it no longer matches your definition, the next ensure returns status code 409. The SDK throws a ToolEnsureConflictError with code: 'external_modification'. Its lastModifiedSource and modifiedAt fields identify the edit. If the edit left the Tool identical to your definition, ensure returns unchanged instead.

Choose one of these resolutions:

  • Keep the dashboard edit: Call pull, review the returned definition as a git diff, and commit it. The next ensure returns unchanged and marks the Tool as managed in code.
  • Keep the repository definition: Pass onConflict: 'overwrite' to replace the dashboard edit.

An overwrite can’t be undone. Tools keep no version history, so the dashboard edit is lost. If you might need it, run pull and save the output before you overwrite.

Pull the saved definition with the following code:

TypeScript SDK
const { toolId, definition, contentHash, lastModifiedSource, updatedAt } =
await Runtype.tools.pull('Weather Lookup')

If no Tool has that name, pull fails with status code 404.

Call the API directly

If you don’t use the TypeScript SDK, send the same requests yourself. Authenticate each request with Authorization: Bearer YOUR_API_KEY, where YOUR_API_KEY is your Runtype API key.

  1. Send POST /v1/tools/ensure with name and the full definition, and omit contentHash. The response includes result and the contentHash that the server computed. Store that contentHash.
  2. On later runs, send a hash-only request with name and the stored contentHash: { "name": "Weather Lookup", "contentHash": "STORED_HASH" }. Replace STORED_HASH with the value from step 1. A match returns { "result": "unchanged" }. A miss returns { "result": "definitionRequired" }, and you repeat step 1.
  3. Handle status code 409 for external_modification and remote_changed the same way as the SDK conflicts. Status code 422 with content_hash_mismatch means you sent both definition and a contentHash that doesn’t match it.

To pull a definition, send GET /v1/tools/pull with the name query parameter. The response includes toolId, definition, contentHash, lastModifiedSource, and updatedAt.

Next steps

Continue with these guides: