Manage tools as code

Keep your Tool definitions in your repository — reviewed, versioned in git, and converged to the platform at deploy time. This is the Tool counterpart of managing Agents as code and managing Flows as code: the same ensure protocol, applied to saved Tools.

Define the tool

defineTool is a pure, local constructor. It validates the definition shape and returns the canonical definition object. The definition surface is the tool’s name, description, type, parameters schema, and config — the same fields you set when creating a tool in the dashboard.

1import { defineTool } from '@runtypelabs/sdk'
2
3export const weatherLookup = defineTool({
4 name: 'Weather Lookup',
5 description: 'Fetch the current weather for a city',
6 toolType: 'external',
7 parametersSchema: {
8 type: 'object',
9 properties: { city: { type: 'string' } },
10 required: ['city'],
11 },
12 config: { url: 'https://api.example.com/weather?q={{city}}', method: 'GET' },
13})

Accepted toolType values are external, custom, graphql, mcp, local, flow, and subagent. The tool’s identity is its name plus your account scope (organization, or personal); which environment you converge is decided by the credentials you run with.

Renaming a definition does not rename the tool. ensure will create a new tool under the new name and leave the old one in place (it never deletes). Treat the name as the stable identity.

flow and subagent tools reference an account-scoped flow or agent ID in their config. Those IDs belong to one environment, so a definition carrying one is not portable across environments the way an external or custom tool is.

Converge at deploy time

1import { Runtype } from '@runtypelabs/sdk'
2
3Runtype.configure({ apiKey: process.env.RUNTYPE_API_KEY })
4
5const result = await Runtype.tools.ensure(weatherLookup)
6// → { result: 'unchanged' | 'created' | 'updated', toolId, contentHash }

ensure is hash-first: in the steady state (nothing changed) it sends one tiny probe request carrying only the name and a content hash, and writes nothing. When the content differs, it creates or updates the saved tool.

The content hash covers the tool’s type, description, parameters schema, and config (the name is identity, not content). The contentHash in every response is computed by the server over its canonical, normalized form; the SDK echoes the server’s hash on later probes.

Tools have no version snapshots (unlike Agents and Flows), so there is no publish option and no versionId on the result. ensure converges the live tool definition directly.

Detect drift in CI

1const plan = await Runtype.tools.ensure(weatherLookup, { dryRun: true })
2// → { result: 'plan', changes: 'none' | 'create' | 'update', changedKeys, contentHash, remoteHash }

changedKeys names what would change (toolType, description, parametersSchema, and config.<key> per changed config entry). For a one-line PR gate, expectNoChanges throws when the plan is anything but none:

1await Runtype.tools.ensure(weatherLookup, { expectNoChanges: true })

To make the apply step act only on the exact state the dry run saw, bind them with expectedRemoteHash:

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

Conflicts with dashboard edits

The platform records where each tool write came from. If someone edits an ensure-managed tool in the dashboard (or via the API), the next ensure fails with a 409 conflict by default, surfaced in the SDK as a ToolEnsureConflictError with code: 'external_modification'. In the dashboard, an ensure-managed tool shows a Managed in code badge, and saving an edit warns you first.

You have two remediation directions:

  • Repo wins. Re-run with onConflict: 'overwrite'. The dashboard edit is overwritten in the saved tool.
  • Dashboard wins. Absorb the edit into your repo with pull, review it as a git diff, and commit or revert:
1const { definition, contentHash, lastModifiedSource, updatedAt } =
2 await Runtype.tools.pull('Weather Lookup')

What ensure does and does not manage

ensure converges the tool definition: its name, description, type, parameters schema, and config (validated with the same custom-tool-code and {{secret:KEY}} reference rules as tool creation). It does not manage which agents or flow steps reference the tool, and it never deletes or renames tools.

Wire protocol (direct API use)

If you are not using the TypeScript SDK, the protocol is one endpoint: POST /v1/tools/ensure.

  1. Probe with { "name": "...", "contentHash": "<sha-256>" }. A match returns { "result": "unchanged" }; a miss returns a normal 200 { "result": "definitionRequired" } — retry with the full definition included.
  2. The server recomputes the canonical hash over any submitted definition and returns it on every response. Echo the server’s hash in later probes.
  3. Conflicts are 409s (external_modification, remote_changed); a submitted contentHash that disagrees with the server’s recompute over the definition is a 422 (content_hash_mismatch). Omit contentHash on full requests to avoid it entirely.

GET /v1/tools/pull?name=... returns the canonical definition plus provenance (contentHash, lastModifiedSource, updatedAt).

Next steps