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

# Creating custom tools

Create a custom Tool to run JavaScript, TypeScript, or Python without calling an external API. Use it for validation, data transformation, or business logic. The Tool receives your defined parameters and returns a result to the Agent.

## Before you begin

Make sure that you have the following:

* A Runtype account with permission to create Tools.
* To use **Daytona Sandbox**, a Daytona API key. For setup steps, see [Configure Daytona](#configure-daytona).
* To create a Tool with the API or the CLI, a Runtype API key.

## Create a custom Tool

To create a custom Tool in the dashboard, follow these steps:

1. In the sidebar, click **Tools**.
2. Click **Create Tool**.
3. Enter the Tool name and description.
4. Select **Custom Code**.
5. Click **Create Tool**. The tool editor opens.

To configure the code and the parameter schema, follow these steps:

1. In **Configuration**, choose an **Execution Environment**.
2. If you chose **Daytona Sandbox**, choose a **Language**.
3. Enter the implementation in the code editor.
4. Open **Parameters** and click **Add Parameter**.
5. Configure each parameter, then click **Save**.

### Create a custom Tool with the API

Send a `POST` request to `/v1/tools` with `toolType` set to `custom`. The following request creates the discount Tool from [Write Tool code](#write-tool-code):

```bash
curl -X POST https://api.runtype.com/v1/tools \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "calculate_discount",
    "description": "Calculates an order discount from the order amount and customer tier.",
    "toolType": "custom",
    "parametersSchema": {
      "type": "object",
      "properties": {
        "orderAmount": { "type": "number", "description": "Total order value in USD." },
        "customerTier": { "type": "string", "description": "standard or premium." }
      },
      "required": ["orderAmount", "customerTier"]
    },
    "config": {
      "code": "const { orderAmount, customerTier } = parameters\nreturn { finalAmount: customerTier === \"premium\" ? orderAmount * 0.85 : orderAmount }",
      "sandboxProvider": "cloudflare-worker",
      "timeout": 30000
    }
  }'
```

Replace `YOUR_API_KEY` with your Runtype API key.

To create the same Tool with the CLI, save the request body as `tool.json` and run the following command:

```bash
runtype tools create --from-file tool.json
```

The name must be 1 to 100 characters. The description is required and must be 1 to 500 characters. The following table describes the `config` fields for a custom Tool:

| Field             | Required | Description                                                                                                                                                             |
| ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`            | Yes      | The Tool implementation.                                                                                                                                                |
| `sandboxProvider` | No       | `cloudflare-worker` (default), `daytona`, `quickjs`, or `runtype-sandbox`.                                                                                              |
| `language`        | No       | `javascript` (default), `typescript`, or `python`. Applies to `daytona` and `runtype-sandbox`.                                                                          |
| `timeout`         | No       | Time limit in milliseconds. We recommend that you set it, because the fallback can be 10 minutes. For the fallbacks, see [Set execution limits](#set-execution-limits). |

For the full request and response schema, see [Create tool](/api-reference/runtype-api/tools/create-tool). To keep Tool definitions in your repository, see [Manage tools as code](/user-guide/tools/manage-tools-as-code).

## Pick an execution environment

Choose an execution environment that supports the language and network behavior that your code needs. The following table compares the environments:

| Execution environment                    | Supported languages            | Network access                           | Use it for                                            |
| ---------------------------------------- | ------------------------------ | ---------------------------------------- | ----------------------------------------------------- |
| **Cloudflare Worker (Default)**          | JavaScript                     | Off unless the Tool sets `networkAccess` | JavaScript with built-in helpers                      |
| **Daytona Sandbox**                      | JavaScript, TypeScript, Python | Follows the Daytona sandbox policy       | Python, TypeScript, or container-based execution      |
| **QuickJS (Legacy)**                     | JavaScript                     | No network access                        | Existing JavaScript tools that use the legacy runtime |
| Runtype Sandbox (not in the tool editor) | JavaScript, TypeScript, Python | Off unless the Tool sets `networkAccess` | Container-based execution with package support        |

Use **Cloudflare Worker (Default)** for JavaScript that uses `async`/`await`, standard built-ins, or the `helpers` namespace. Runtype captures `console.log` output in execution logs. A network call such as `fetch` fails with an error unless the Tool sets `networkAccess`. For details, see [Call an API from custom code](#call-an-api-from-custom-code).

Use **Daytona Sandbox** for TypeScript or Python in the dashboard. Runtype injects each parameter as a variable in your code.

Use **QuickJS (Legacy)** to maintain existing JavaScript tools. QuickJS runs JavaScript synchronously and exposes the helper functions as global functions instead of the `helpers` namespace.

The tool editor doesn't list Runtype Sandbox. To use it, set `sandboxProvider` to `runtype-sandbox` and set `language` when you create or update the Tool with the API. Your code reads the `parameters` object. Runtype Sandbox code can import preinstalled packages such as numpy and pandas. For the full list, see [Preinstalled sandbox software](/developer-guides/guides/sandbox-preinstalled-software).

## Call an API from custom code

Custom code has no network access by default. This includes every Tool that existed before the `networkAccess` setting. Tool parameters can carry text from a model or a user, so a Tool gets network access only when you turn it on for that Tool.

To let the code call an API, set `networkAccess` in the Tool `config`:

* `"off"` blocks all requests. This is the default.
* `{ "allowedHostnames": ["api.github.com"] }` allows requests to the listed hosts only. A `*.` prefix, such as `*.github.com`, matches subdomains. Prefer this form.
* `"on"` allows requests to any public host.

To send a managed secret, declare it in `secrets`. Your code never receives the secret value. It receives a placeholder token as `secrets.NAME`, and the egress proxy replaces the placeholder with the real value only on requests to the declared host:

```json
{
  "code": "const res = await fetch('https://api.github.com/user', { headers: { Authorization: `Bearer ${secrets.GITHUB_TOKEN}` } }); return await res.json()",
  "networkAccess": { "allowedHostnames": ["api.github.com"] },
  "secrets": [
    { "key": "GITHUB_TOKEN", "hostnamePattern": "api.github.com", "injectionMode": "bearer_header" }
  ]
}
```

Each `secrets` entry takes these fields:

| Field             | Description                                                                                                                                                                    |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `key`             | The managed secret name, such as `GITHUB_TOKEN`. Don't wrap it in `{{secret:...}}`.                                                                                            |
| `hostnamePattern` | The one host, or `*.` domain, that receives the secret. `networkAccess` must allow this host. A wildcard over a public suffix, such as `*.co.uk` or `*.github.io`, is refused. |
| `pathPattern`     | Optional. Limits the secret to matching paths. A trailing `*` matches a prefix. The default is every path.                                                                     |
| `injectionMode`   | Where the proxy puts the secret: `bearer_header`, `api_key_header`, `query_param`, or `basic_auth`.                                                                            |
| `injectionConfig` | Optional. `headerName` for `api_key_header`, `paramName` for `query_param`, or `username` for `basic_auth`.                                                                    |

A Tool can declare up to 10 secrets, each secret at most once. The proxy blocks a request that sends the placeholder to any other host or path. `networkAccess` and `secrets` work with the Cloudflare Worker and Runtype Sandbox environments. QuickJS has no network access, and Daytona Sandbox can't inject secrets, so Runtype rejects these settings for those environments. Creating or dispatching a Tool with `secrets` requires the `SECRETS:READ` permission.

You can't write `{{secret:NAME}}` in custom code. Use `secrets` instead.

### Configure Daytona

To add a Daytona API key, follow these steps:

1. Open **Settings** and select **Integrations**.
2. Under **Code Execution**, enter the Daytona API key.
3. Click **Save Key**.

If you don't add a key, Runtype uses a platform key when one is available for your organization. If no Daytona API key is available, either your own or a platform key, the Tool returns an error.

## Write Tool code

The runtime provides input in different ways. **Cloudflare Worker**, **QuickJS**, and Runtype Sandbox code reads the `parameters` object. **Daytona Sandbox** injects each parameter as a variable.

A code Tool reads its arguments from `parameters`. For a Tool with an `orderId` parameter, read `parameters.orderId`.

In **Cloudflare Worker** and **QuickJS** JavaScript, the `input` variable is not the arguments object. It is a wrapper that holds `parameters` (and `secrets` when the Tool has secret bindings), so `input.parameters.orderId` returns the same value as `parameters.orderId`, and `input.orderId` is `undefined`. The shape is the same for every caller: the execute endpoint, the MCP `execute_tool` tool, the SDK, the CLI, the dashboard **Test** panel, an Agent, and a Flow.

This differs from a Flow's transform-data step. In a transform-data step, `input` is the flat bag of Flow variables, so `input.orderId` works there. Do not copy that pattern into a Tool.

Use this example with **Cloudflare Worker**, **QuickJS**, or Runtype Sandbox:

```javascript
const { orderAmount, customerTier } = parameters

let discount = 0
if (customerTier === 'premium') {
  discount = orderAmount > 1000 ? 0.2 : 0.15
} else if (customerTier === 'standard') {
  discount = orderAmount > 1000 ? 0.1 : 0.05
}

const discountedAmount = orderAmount * (1 - discount)

return {
  originalAmount: orderAmount,
  discount: discount * 100,
  finalAmount: discountedAmount,
}
```

Use this Python example with Daytona to aggregate an array of numbers. Define a required parameter named `numbers` with type `array` before you run the example:

```python
import json
import statistics

result = {
    'sum': sum(numbers),
    'mean': statistics.mean(numbers),
    'median': statistics.median(numbers),
    'min': min(numbers),
    'max': max(numbers),
    'count': len(numbers)
}

print(json.dumps(result))
```

Use the language runtime and packages that **Daytona Sandbox** provides. Outbound network access follows its sandbox policy.

### Runtime behavior

How your code sends a result depends on the environment and the language. The following table shows the rule for each combination:

| Environment                        | Language                       | How to send the result |
| ---------------------------------- | ------------------------------ | ---------------------- |
| **Cloudflare Worker**, **QuickJS** | JavaScript                     | Return a value.        |
| Runtype Sandbox                    | JavaScript, TypeScript         | Return a value.        |
| Runtype Sandbox                    | Python                         | Print one JSON value.  |
| **Daytona Sandbox**                | JavaScript, TypeScript, Python | Print one JSON value.  |

Runtype parses printed output as JSON when possible and otherwise passes the raw text to the Agent. In **Daytona Sandbox**, `return` doesn't send a result. Print the value instead.

The runtime handles errors and logs as follows:

* Handle expected failures with a structured error object. An unhandled exception fails the Tool and returns an error to the Agent.
* Use `console.log` in **Cloudflare Worker** or **QuickJS** code when you need output in execution logs.

### Available JavaScript features

**Cloudflare Worker** provides these JavaScript features:

* Standard built-ins such as `Array`, `Object`, `Math`, `Date`, and `JSON`.
* `async`/`await`, arrow functions, destructuring, and template literals.
* Regular expressions and string manipulation.
* The `helpers` namespace.
* Captured `console.log` output in execution logs.

**QuickJS** provides the following JavaScript features:

* Standard built-ins such as `Array`, `Object`, `Math`, `Date`, and `JSON`.
* Regular expressions and string manipulation.
* Helper functions such as `parseHTML()` and `extractEmails()` as global functions.
* Synchronous JavaScript execution.

**Daytona Sandbox** provides the language runtime and container capabilities in its configuration. Use the selected language's standard library and available packages.

### Cloudflare Worker helper functions

**Cloudflare Worker** exposes the following functions in the `helpers` namespace:

| Helper                              | Description                                                                   |
| ----------------------------------- | ----------------------------------------------------------------------------- |
| `helpers.parseHTML(html)`           | Parses HTML and returns a title, headings, links, text, and selector methods. |
| `helpers.extractEmails(text)`       | Extracts email addresses from text.                                           |
| `helpers.extractURLs(text)`         | Extracts URLs from text.                                                      |
| `helpers.extractPhoneNumbers(text)` | Extracts phone numbers from text.                                             |
| `helpers.formatDate(date, format)`  | Formats a date with `YYYY-MM-DD HH:mm:ss` patterns.                           |
| `helpers.parseDate(dateStr)`        | Parses a date string and returns a `Date` object or `null`.                   |
| `helpers.addDays(date, days)`       | Adds or subtracts days from a date.                                           |
| `helpers.truncate(text, length)`    | Truncates text to the specified length.                                       |
| `helpers.slugify(text)`             | Converts text into a URL-friendly slug.                                       |
| `helpers.markdown2html(md)`         | Converts basic Markdown into HTML.                                            |
| `helpers.html2markdown(html)`       | Converts basic HTML into Markdown.                                            |
| `helpers.isValidEmail(email)`       | Checks whether a string matches the supported email format.                   |
| `helpers.isValidURL(url)`           | Checks whether a string is a valid URL.                                       |
| `helpers.isValidJSON(str)`          | Checks whether a string contains valid JSON.                                  |
| `helpers.base64Encode(str)`         | Encodes a string as base64.                                                   |
| `helpers.base64Decode(str)`         | Decodes a base64 string.                                                      |
| `helpers.urlEncode(str)`            | URL-encodes a string.                                                         |
| `helpers.urlDecode(str)`            | URL-decodes a string.                                                         |

In **QuickJS**, call these helper functions without the `helpers.` prefix.

## Define parameters

Open **Parameters**, then click **Add Parameter** to define the input schema. Configure each field as follows:

| Field             | Purpose                                                          |
| ----------------- | ---------------------------------------------------------------- |
| **Name**          | Names the parameter that your code reads.                        |
| **Type**          | Sets `string`, `number`, `boolean`, `object`, or `array`.        |
| **Description**   | Explains the value that the Agent must provide.                  |
| **Required**      | Marks the parameter as mandatory.                                |
| **Default Value** | Documents a default in the parameter schema that the Agent sees. |

Runtype doesn't fill in a default value when the Agent omits a parameter. Handle a missing value in your code, for example `const format = parameters.format ?? 'iso'`.

For structured input, use type `object` or `array`, not a `string` that holds JSON. Models often drop the escape characters in a JSON string, and the call fails. When a string parameter looks like a JSON payload, the API create and update responses include a `TOOL_PARAM_EMBEDDED_JSON_STRING` entry in `validation.recommendations`.

For **Daytona Sandbox**, name each parameter with letters, digits, and underscores, and start the name with a letter or an underscore. Don't use a Python built-in name such as `open` or `eval`, or a name that starts and ends with two underscores. Runtype doesn't inject a parameter with any other name, such as `order-id`, so your code fails when it reads that name.

Use this parameter schema with the discount example:

| Name           | Type     | Required | Description               |
| -------------- | -------- | -------- | ------------------------- |
| `orderAmount`  | `number` | Yes      | Total order value in USD. |
| `customerTier` | `string` | Yes      | `standard` or `premium`.  |

## Set execution limits

Set **Timeout (ms)** between `1,000` and `300,000` in the tool editor. A Tool created in the dashboard starts at `30,000` milliseconds. The Tool stops when it reaches the timeout. These limits apply to saved Tools. A runtime custom Tool that you define in a dispatch request has a maximum of 30 seconds.

When you create a Tool with the API and don't set `timeout`, the limit depends on the environment:

* **Cloudflare Worker**: `600,000` milliseconds.
* **QuickJS**: `5,000` milliseconds.
* Runtype Sandbox: `30,000` milliseconds.

**Timeout (ms)** doesn't apply to **Daytona Sandbox**, which uses its own sandbox execution limit.

The tool editor also shows a **Memory Limit** control. This setting doesn't change how the Tool runs.

## Validate Tool code

Runtype validates custom code before it saves a Tool. JavaScript and TypeScript use syntax and semantic checks. Python uses syntax checks.

Validation errors block a save. The following checks produce errors:

* `eval()`, with the error code `USE_OF_EVAL`.
* `new Function()`, with the error code `USE_OF_FUNCTION_CTOR`.
* A top-level declaration named `input` in **Cloudflare Worker** or **Daytona Sandbox** JavaScript or TypeScript, with the error code `RESERVED_NAME_REDECLARED`.
* Syntax errors.

Validation warnings don't block a save. The following checks produce warnings:

* A loop with a constant-true condition and no `break` or `await`, such as `while (true)` or `for (;;)`, with the warning code `INFINITE_LOOP`.
* A dynamic `import()` expression, with the warning code `DYNAMIC_IMPORT`.
* A missing top-level `return` statement in **Cloudflare Worker** or **QuickJS** code, with the warning code `RETURN_UNDEFINED`.
* A declaration that hides a name the runtime provides, such as `helpers`, with the warning code `SHADOWED_WRAPPER_NAME`.

The tool editor shows errors and warnings under the code. The API returns them as follows:

* A blocked save returns HTTP `400` with `error` set to `Custom tool code failed validation`, plus `errors` and `warnings` arrays.
* A successful create or update that has warnings returns them in `validation.warnings`.

Runtype runs the validation again whenever you update the Tool.

## Handle errors

Return a structured error object when the Agent can recover from an expected input error. Use this JavaScript example:

```javascript
const { email } = parameters

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
  return { success: false, error: 'Invalid email format' }
}

return { success: true, isValid: true, email }
```

## Test a Tool

To run a test from the tool editor, follow these steps:

1. Open **Test Tool**.
2. Enter sample values in the **Params** tab.
3. Click **Run Test**.
4. Review the status, result, error, and execution time in the **Result** tab.
5. Optional: Open the **History** tab to review previous tests.

To test a Tool from the command line, run the following command:

```bash
runtype tools execute <tool-id> --input '{"orderAmount": 1200, "customerTier": "premium"}'
```

Replace `<tool-id>` with the ID of the Tool. The command sends a `POST` request to `/v1/tools/{id}/execute` with the input in a `parameters` object. For the request schema, see [Execute tool](/api-reference/runtype-api/tools/execute-tool).

The following table describes the fields in the result:

| Field             | Description                                  |
| ----------------- | -------------------------------------------- |
| `executionId`     | The ID of this run.                          |
| `status`          | `success` or `failed`.                       |
| `result`          | The value that the Tool returned or printed. |
| `error`           | The error message when the run fails.        |
| `executionTimeMs` | The run time in milliseconds.                |

## Troubleshoot

The following table lists common failures, their causes, and fixes:

| Symptom                                                                                       | Cause                                                                         | Fix                                                                                                |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `Daytona API key not configured. Please add your Daytona API key in Settings > Integrations.` | No Daytona API key is available.                                              | Add a key. For steps, see [Configure Daytona](#configure-daytona).                                 |
| A `fetch` call fails in **Cloudflare Worker** code.                                           | The Tool doesn't allow network access to that host.                           | Set `networkAccess`. For steps, see [Call an API from custom code](#call-an-api-from-custom-code). |
| A `NameError` or `ReferenceError` for a parameter in **Daytona Sandbox**.                     | The parameter name isn't a valid variable name, so Runtype doesn't inject it. | Rename the parameter. For the naming rules, see [Define parameters](#define-parameters).           |
| A **Daytona Sandbox** Tool fails with error output from your code.                            | The code exited with a non-zero exit code.                                    | Read the error text, fix the code, and test again.                                                 |
| A **Daytona Sandbox** Tool returns no result.                                                 | The code returns the value instead of printing it.                            | Print one JSON value.                                                                              |

## Apply best practices

Use these practices when you create a custom Tool:

* Give each Tool one responsibility so you can isolate failures.
* Validate parameter values before you process them.
* Name each Tool after its operation, such as `calculate_shipping_cost`.
* Return structured objects so the Agent can use individual fields.
* Describe the Tool purpose, usage condition, output, limitations, and side effects.

## Examples

### Email validator

Use this JavaScript example to validate an email address:

```javascript
const { email } = parameters
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return {
  isValid: emailRegex.test(email),
  email,
}
```

Define one required `email` parameter with type `string`.

### Date formatter

Use this JavaScript example to format an ISO date. When the Agent omits `format`, the code uses `iso`:

```javascript
const { isoDate } = parameters
const format = parameters.format ?? 'iso'
const date = new Date(isoDate)

if (format === 'short') {
  return { formatted: date.toLocaleDateString() }
} else if (format === 'long') {
  return {
    formatted: date.toLocaleDateString('en-US', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric',
    }),
  }
}

return { formatted: date.toISOString() }
```

Define `isoDate` as a required `string` parameter and `format` as an optional `string` parameter.

## Next steps

Use these links to continue:

* [Creating external tools](/user-guide/tools/creating-external-tools): call an HTTP API from an Agent.
* [Manage tools as code](/user-guide/tools/manage-tools-as-code): keep custom Tool definitions in your repository.
* [Runtime tools](/user-guide/tools/runtime-tools): define a custom Tool inline in a dispatch request.
* [Agent tools](/user-guide/agents/agent-tools): attach and configure Tools.
* [Built-in tools](/user-guide/tools/built-in-tools): add ready-to-use integrations.
* [What are Tools?](/user-guide/tools/what-are-tools): compare Tool types.
* [Integrations (PostHog, Weaviate, Daytona)](/user-guide/settings/integrations-posthog-weaviate-daytona): configure a Daytona API key.
* [Designing tools for agents](/user-guide/tools/designing-tools-for-agents): write tool names, descriptions, and parameters that Agents use well.