Skip to navigation

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

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:

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:

FieldRequiredDescription
codeYesThe Tool implementation.
sandboxProviderNocloudflare-worker (default), daytona, quickjs, or runtype-sandbox.
languageNojavascript (default), typescript, or python. Applies to daytona and runtype-sandbox.
timeoutNoTime limit in milliseconds. We recommend that you set it, because the fallback can be 10 minutes. For the fallbacks, see Set execution limits.

For the full request and response schema, see Create tool. To keep Tool definitions in your repository, see 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 environmentSupported languagesNetwork accessUse it for
Cloudflare Worker (Default)JavaScriptOff unless the Tool sets networkAccessJavaScript with built-in helpers
Daytona SandboxJavaScript, TypeScript, PythonFollows the Daytona sandbox policyPython, TypeScript, or container-based execution
QuickJS (Legacy)JavaScriptNo network accessExisting JavaScript tools that use the legacy runtime
Runtype Sandbox (not in the tool editor)JavaScript, TypeScript, PythonOff unless the Tool sets networkAccessContainer-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.

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.

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:

{
"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:

FieldDescription
keyThe managed secret name, such as GITHUB_TOKEN. Don’t wrap it in {{secret:...}}.
hostnamePatternThe 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.
pathPatternOptional. Limits the secret to matching paths. A trailing * matches a prefix. The default is every path.
injectionModeWhere the proxy puts the secret: bearer_header, api_key_header, query_param, or basic_auth.
injectionConfigOptional. 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:

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:

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:

EnvironmentLanguageHow to send the result
Cloudflare Worker, QuickJSJavaScriptReturn a value.
Runtype SandboxJavaScript, TypeScriptReturn a value.
Runtype SandboxPythonPrint one JSON value.
Daytona SandboxJavaScript, TypeScript, PythonPrint 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:

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

FieldPurpose
NameNames the parameter that your code reads.
TypeSets string, number, boolean, object, or array.
DescriptionExplains the value that the Agent must provide.
RequiredMarks the parameter as mandatory.
Default ValueDocuments 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:

NameTypeRequiredDescription
orderAmountnumberYesTotal order value in USD.
customerTierstringYesstandard 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:

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:

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.

The following table describes the fields in the result:

FieldDescription
executionIdThe ID of this run.
statussuccess or failed.
resultThe value that the Tool returned or printed.
errorThe error message when the run fails.
executionTimeMsThe run time in milliseconds.

Troubleshoot

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

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

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:

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: