Skip to Content
ToolsJavaScript tools

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

  1. Open Tools and click Create tool.
  2. Under Execution method, pick Custom JavaScript.
  3. Give it a Tool name and a Description that says when to use it.
  4. Under Parameters, list the inputs, such as price, quantity and tier.
  5. Write the code under JavaScript code. Read inputs as parameters.price, parameters.tier and so on, and return the result.
  6. Choose who can use it under Visibility and click Create tool.
  7. 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:

ArgumentDescription
parametersThe tool’s inputs
credentialsValues of the credential the tool, or its linked API, uses
requireLoads npm packages and an allowlist of Node built-ins
toolstools.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 rows

Packages’ 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:

CategoryBlocked
IPv4 RFC 1918 private ranges (10/8, 172.16/12, 192.168/16)Yes
CGNAT 100.64/10Yes
Loopback 127/8Yes
Link-local 169.254/16 (includes cloud metadata services)Yes
IPv6 loopback, unspecified, link-local, ULAYes
Metadata hostnames (metadata.google.internal, etc.)Yes
Unix domain socketsYes
Public unicast IPv4 and IPv6Allowed

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

LimitValue
Run time30 s by default (configuration.timeout, in milliseconds), at most 300 s
Memory128 MB heap
Concurrent workers4 per install (SANDBOX_MAX_WORKERS), half of them for one organization (SANDBOX_MAX_WORKERS_PER_ORG)
Waiting runs100 (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'.