JavaScript tools
A JavaScript tool runs a short piece of code you write. Use it for work no API does for you: work out a discount, tidy up a messy list before an assistant reads it, or call a service through its JavaScript library. For example, a calculate_discount tool can take a price, a quantity and a customer tier and return the total.
The code runs in a locked-down space (a sandbox) on almyty’s servers: it cannot read the server’s files, start programs or reach private networks.
Create one
- Open Tools and click Create tool.
- Under Execution method, pick Custom JavaScript.
- Give it a Tool name and a Description that says when to use it.
- Under Parameters, list the inputs, such as
price,quantityandtier. - Write the code under JavaScript code. Read inputs as
parameters.price,parameters.tierand so on, andreturnthe result. - Choose who can use it under Visibility and click Create tool.
- Open the tool, go to Test tool, fill in sample inputs and click Execute Tool to check the answer.
A small example:
const discounts = { bronze: 0.05, silver: 0.10, gold: 0.20 }
const subtotal = parameters.price * parameters.quantity
const discount = subtotal * (discounts[parameters.tier] || 0)
return { subtotal, discount, total: subtotal - discount }To report a problem, throw new Error('...'): the message comes back to the assistant as a failed call.
For developers
What the code can use
The code is the body of an async function with four arguments:
| Argument | Description |
|---|---|
parameters | The tool’s inputs |
credentials | Values of the credential the tool, or its linked API, uses |
require | Loads npm packages and an allowlist of Node built-ins |
tools | tools.invoke('tool_id', { ... }) calls another tool, when nested invocation is available |
Return any JSON-serializable value.
The tool definition
{
"name": "calculate_discount",
"description": "Calculate discount price based on tier and quantity",
"type": "function",
"executionMethod": "custom",
"parameters": {
"type": "object",
"properties": {
"price": { "type": "number", "description": "Original price" },
"quantity": { "type": "integer", "description": "Number of items" },
"tier": { "type": "string", "enum": ["bronze", "silver", "gold"], "description": "Customer tier" }
},
"required": ["price", "quantity", "tier"]
},
"code": "const discounts = { bronze: 0.05, silver: 0.10, gold: 0.20 };\nconst subtotal = parameters.price * parameters.quantity;\nconst discount = subtotal * (discounts[parameters.tier] || 0);\nreturn { subtotal, discount, total: subtotal - discount };"
}npm packages
List packages in the tool’s dependencies field (name to version). They are installed into a per-tool cache on first run and reused afterwards:
// Tool declares: { "dependencies": { "pg": "^8.20.0" } }
const { Client } = require('pg')
const client = new Client({
host: credentials.host,
user: credentials.user,
password: credentials.password,
database: credentials.database,
})
await client.connect()
const { rows } = await client.query('SELECT * FROM orders WHERE user_id = $1', [parameters.userId])
await client.end()
return rowsPackages’ own internal requires use the worker’s native require, so a package that uses http or net still works; every outbound connection still goes through the network guard.
Built-in modules
require() of Node built-ins follows an allowlist:
crypto, buffer, util, url, querystring, string_decoder,
punycode, events, stream, stream/web, stream/promises, timers,
timers/promises, zlib, assert, path, async_hooks
Anything else (fs, http, https, net, tls, dgram, dns, child_process, worker_threads, vm, inspector, os, module and the rest) is refused at require() time. For HTTP, use the global fetch() or an installed package such as axios, got or undici.
Sandbox
Each run gets its own Node.js worker thread under the permission model (--permission), with a network guard installed before any user code loads. The threat model assumes adversarial code.
Files. The worker may read only the tool’s installed dependencies and its own bootstrap script. Reading /etc/passwd, /proc or /root, writing anywhere, spawning a child process, launching a nested worker and loading a native .node addon all fail with ERR_ACCESS_DENIED.
Network. The guard patches dns.lookup, net.Socket.prototype.connect and dgram.Socket.prototype.send before user code runs, and classifies every connection, whether from your code or a dependency:
| Category | Blocked |
|---|---|
| IPv4 RFC 1918 private ranges (10/8, 172.16/12, 192.168/16) | Yes |
| CGNAT 100.64/10 | Yes |
| Loopback 127/8 | Yes |
| Link-local 169.254/16 (includes cloud metadata services) | Yes |
| IPv6 loopback, unspecified, link-local, ULA | Yes |
Metadata hostnames (metadata.google.internal, etc.) | Yes |
| Unix domain sockets | Yes |
| Public unicast IPv4 and IPv6 | Allowed |
A refused connection throws an Error with code: ERR_SANDBOX_NET_REFUSED.
Environment. process.env is emptied before your code runs. Secrets come through credentials.
Nested calls. tools.invoke calls another tool without an HTTP round trip. Each nested call runs in a fresh worker with the same organization, network and file guards.
const price = await tools.invoke('lookup_price', { sku: parameters.sku })
return tools.invoke('calculate_discount', { price: price.price, tier: parameters.tier, quantity: parameters.quantity })Limits
| Limit | Value |
|---|---|
| Run time | 30 s by default (configuration.timeout, in milliseconds), at most 300 s |
| Memory | 128 MB heap |
| Concurrent workers | 4 per install (SANDBOX_MAX_WORKERS), half of them for one organization (SANDBOX_MAX_WORKERS_PER_ORG) |
| Waiting runs | 100 (SANDBOX_MAX_QUEUE_SIZE) |
Running out of time or memory stops the worker and returns success: false, with an out-of-memory flag in the memory case.
Errors and cancellation
Thrown errors, and sandbox refusals (permission denied, network refused, module not allowed), come back as a failed result with the message. If the caller goes away (the client disconnects, the agent run is cancelled, a queue times out), the worker is stopped and the result is success: false with error: 'Sandbox execution cancelled'.