> 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 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](/user-guide/agents/manage-agents-as-code) and [Manage Flows as code](/user-guide/flows/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](/user-guide/settings/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`**

```typescript title="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](/user-guide/settings/managing-secrets).

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

> **Note**
>
> `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`**

```typescript title="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](#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](/user-guide/tools/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](#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`**

```typescript title="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`**

```typescript title="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`**

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

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

```typescript title="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:

* [Manage Agents as code](/user-guide/agents/manage-agents-as-code): apply Agent definitions from your repository
* [Manage Flows as code](/user-guide/flows/manage-flows-as-code): apply Flow definitions from your repository
* [Creating custom tools](/user-guide/tools/creating-custom-tools): write the code for `custom` Tools
* [Creating external tools](/user-guide/tools/creating-external-tools): configure saved HTTP tools in the dashboard
* [Managing secrets](/user-guide/settings/managing-secrets): store credentials for `{{secret:...}}` references