> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runtools.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Tools

> Turn any API into a hosted tool your agents can call — with defineTool()

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/`:

```typescript tools/weather.ts theme={null}
import { defineTool } from '@runtools-ai/sdk';

export default defineTool({
  name: 'weather',
  description: 'Look up current weather for a city',
  category: 'utilities',
  credentials: {
    required: ['WEATHER_API_KEY'],
    schema: {
      WEATHER_API_KEY: { type: 'string', description: 'OpenWeather API key' },
    },
  },
  actions: {
    current: {
      description: 'Current conditions for a city',
      parameters: {
        type: 'object',
        properties: { city: { type: 'string', description: 'City name' } },
        required: ['city'],
      },
      execute: async (params, credentials) => {
        const res = await fetch(
          `https://api.openweathermap.org/data/2.5/weather` +
            `?q=${encodeURIComponent(String(params.city))}&appid=${credentials.WEATHER_API_KEY}`,
        );
        if (!res.ok) throw new Error(`Weather lookup failed (${res.status})`);
        return res.json();
      },
    },
  },
});
```

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](#credentials).)
* **`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:

<Check>**You get the modern web platform.** `fetch`, `crypto`, `Buffer`, `TextEncoder`, `URL` — all global, all standard.</Check>

<Check>**You can import real packages.** Use Deno specifiers: `npm:`, `node:`, or `jsr:`. See [Using packages](#using-packages).</Check>

<Warning>**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`, …).</Warning>

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

<Info>**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.</Info>

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:

```typescript tools/postgres-report.ts theme={null}
import postgres from 'npm:postgres';
import { defineTool } from '@runtools-ai/sdk';

export default defineTool({
  name: 'postgres-report',
  description: 'Run a read-only report query',
  category: 'database',
  credentials: {
    required: ['POSTGRES_URL'],
    schema: { POSTGRES_URL: { type: 'string', description: 'Postgres connection string' } },
  },
  actions: {
    query: {
      description: 'Run a SQL query',
      parameters: {
        type: 'object',
        properties: { sql: { type: 'string', description: 'SQL to run' } },
        required: ['sql'],
      },
      execute: async (params, credentials) => {
        const db = postgres(credentials.POSTGRES_URL, { max: 1 });
        try {
          return { rows: await db.unsafe(String(params.sql)) };
        } finally {
          await db.end({ timeout: 5 });
        }
      },
    },
  },
});
```

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

```typescript theme={null}
await rt.tools.storeCredentials('weather', { WEATHER_API_KEY: process.env.OPENWEATHER! });
```

<Tip>Name credential fields like environment variables — `STRIPE_API_KEY`, `POSTGRES_URL` — not vague labels like `key`. It reads better everywhere the field surfaces.</Tip>

### 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:

```typescript theme={null}
credentials: {
  required: ['GITHUB_TOKEN'],
  schema: { GITHUB_TOKEN: { type: 'string', description: 'GitHub token' } },
  oauth: {
    provider: 'github',                                // a Runtools-supported provider
    scopes: ['repo'],
    credentialMapping: { GITHUB_TOKEN: 'access_token' }, // your field ← the OAuth token field
  },
},
```

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](/advanced/oauth) for the full provider list and account selection.

## Deploy & publish

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

```typescript runtools.config.ts theme={null}
import { defineConfig } from '@runtools-ai/sdk';

export default defineConfig({
  project: { name: 'my-tools', org: 'my-org' },
  toolsDir: './tools',
});
```

<Steps>
  <Step title="Deploy">
    Ships every tool in `tools/` to your org (private by default). `--tools-only` skips agents and sandboxes.

    ```bash theme={null}
    runtools deploy --tools-only
    runtools tool list --custom
    ```
  </Step>

  <Step title="Choose who can see it">
    New tools are private to you. Open them up when you're ready:

    ```bash theme={null}
    runtools tool publish weather --org    # whole organization
    runtools tool publish weather          # public marketplace (org admin)
    runtools tool publish weather --unpublish   # back to private
    ```

    From the SDK: `await rt.tools.setVisibility('weather', 'org')`.
  </Step>
</Steps>

`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:

```typescript SDK theme={null}
const result = await rt.tools.execute('weather', {
  action: 'current',
  params: { city: 'Lisbon' },
});
```

```bash CLI theme={null}
runtools tool exec weather --action current --params '{"city":"Lisbon"}'
```

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

<AccordionGroup>
  <Accordion title="Return errors agents can recover from">
    `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.
  </Accordion>

  <Accordion title="Spend effort on your parameter schemas">
    The schema is the tool's instructions to the model. Tight `description`s, `enum`s for fixed choices, and honest `required` lists turn flaky tool calls into reliable ones. It's the highest-leverage thing you can do.
  </Accordion>

  <Accordion title="Let the platform hold secrets">
    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.
  </Accordion>

  <Accordion title="Make actions idempotent where you can">
    Agents retry. A `create_*` action that's safe to call twice — or that dedupes on a key — saves you a class of confusing bugs.
  </Accordion>
</AccordionGroup>

## See it in practice

The first-party catalog is real, deployed tools using exactly this API — the best reference there is. Browse **[runtools-official](https://github.com/runtools-ai/runtools-official)** (`tools/`):

<CardGroup cols={2}>
  <Card title="OAuth, dual-mode" icon="key">
    `github.ts`, `slack.ts`, `dropbox.ts` — `oauth` + `credentialMapping`.
  </Card>

  <Card title="Database drivers" icon="database">
    `postgresql.ts`, `mysql.ts` — `npm:` packages in action.
  </Card>

  <Card title="Exact-key SaaS" icon="plug">
    `stripe.ts`, `twilio.ts`, `notion.ts` — one key, REST over `fetch`.
  </Card>

  <Card title="Use your tools" icon="wrench" href="/sdk/tools">
    Search, install, configure, and execute with `rt.tools`.
  </Card>
</CardGroup>
