Skip to main content
A tool is a typed capability — a named action, a JSON-schema for its inputs, and an execute function that does the work. You write one file, run runtools deploy, and that capability is now callable from your agents, the API, the CLI, and the dashboard. Runtools hosts it, resolves its credentials, and runs it in an isolated sandbox so you never ship a server. If you can write a fetch, you can write a tool.

Your first tool

Every tool is a default export from a file under tools/:
tools/weather.ts
Three things are doing the work here:
  • parameters is JSON Schema. It’s the contract an agent sees, so the better it describes the inputs, the more reliably the model calls your tool. params arrives already shaped to match it.
  • credentials declares what the tool needs to run. You never read secrets yourself — Runtools resolves them and hands them to execute as the second argument. (More on that below.)
  • execute is your logic. Return anything JSON-serializable; throw to surface a failure to the caller.
That’s the whole API. The interesting part is where this code runs.

Where your code runs

Your execute doesn’t run on your laptop or inside the credential-bearing tools service. Every call spins up a fresh, isolated Deno sandbox — its own V8 isolate, no secrets, torn down when the call returns. That isolation is the point: a tool can misbehave and the blast radius is one throwaway sandbox. A few rules fall out of that, and they’re worth internalizing before you write anything non-trivial:
You get the modern web platform. fetch, crypto, Buffer, TextEncoder, URL — all global, all standard.
You can import real packages. Use Deno specifiers: npm:, node:, or jsr:. See Using packages.
No Bun-runtime builtins. import { SQL } from 'bun' will not resolve — bun is a runtime intrinsic, not a package. Reach for an npm: driver instead (npm:postgres, npm:mysql2/promise, …).
Secrets come as the credentials argument, never from the environment. Reading process.env / Deno.env inside a tool returns undefined by design — there’s nothing there to leak. Use the credentials parameter.
The network is open but guarded. Call any public API over fetch. Requests to cloud-metadata endpoints, loopback, and private IP ranges are blocked — there’s nothing internal for a tool to reach anyway. Filesystem and subprocesses are off.
Tools are TypeScript/JavaScript today. (Python authoring isn’t supported yet.)

Using packages

Need a database driver, a crypto library, an SDK? Import it with a Deno specifier — Runtools resolves and caches it server-side, so there’s no install or bundling step on your end:
tools/postgres-report.ts
node: builtins (node:buffer, node:crypto) and jsr: modules work the same way. The first call that needs a brand-new package pays a one-time cold fetch; after that it’s warm.

Credentials

Declare what a tool needs and Runtools fills it in at call time — from a stored key, a named secret, or a connected OAuth account — and passes it to execute. Your code just reads credentials.X.

Manual keys

The simplest case: the user stores a value for each required field, via the dashboard, the CLI, or the SDK.
Name credential fields like environment variables — STRIPE_API_KEY, POSTGRES_URL — not vague labels like key. It reads better everywhere the field surfaces.

OAuth (Connected Apps)

For user-owned accounts, add an oauth block. When the user has connected the provider, Runtools mints a fresh token server-side and maps it onto your credential field — no manual key, no refresh logic, no tokens in your code:
Declare both and your tool is dual-mode: it uses the connected account when there is one, and falls back to a manual key otherwise. execute reads credentials.GITHUB_TOKEN either way and never knows the difference. See Connected Apps for the full provider list and account selection.

Deploy & publish

Tools live in a project alongside a runtools.config.ts:
runtools.config.ts
1

Deploy

Ships every tool in tools/ to your org (private by default). --tools-only skips agents and sandboxes.
2

Choose who can see it

New tools are private to you. Open them up when you’re ready:
From the SDK: await rt.tools.setVisibility('weather', 'org').
deploy ships your tool’s source — the sandbox resolves imports and runs it, so there’s nothing to build or bundle first.

Calling it

Once deployed, call it like any other tool:
SDK
CLI
Responses are normalized to { success, result, durationMs } (or { success: false, error }). Agents call your tool through the exact same path — deploy once, and it’s available to every agent in the org.

Patterns that hold up

throw for hard failures (bad credentials, 500s). For recoverable provider problems an agent might route around — a 404, a rate limit — return { success: false, error: '...' } so the model can read it and adapt instead of crashing the run.
The schema is the tool’s instructions to the model. Tight descriptions, enums for fixed choices, and honest required lists turn flaky tool calls into reliable ones. It’s the highest-leverage thing you can do.
Never hardcode a key or read it from env (you can’t anyway). Declare it in credentials, or wire OAuth. One source of truth, rotatable without a redeploy.
Agents retry. A create_* action that’s safe to call twice — or that dedupes on a key — saves you a class of confusing bugs.

See it in practice

The first-party catalog is real, deployed tools using exactly this API — the best reference there is. Browse runtools-official (tools/):

OAuth, dual-mode

github.ts, slack.ts, dropbox.tsoauth + credentialMapping.

Database drivers

postgresql.ts, mysql.tsnpm: packages in action.

Exact-key SaaS

stripe.ts, twilio.ts, notion.ts — one key, REST over fetch.

Use your tools

Search, install, configure, and execute with rt.tools.