Tools v4.25.0+
Let code in a Sandbox call back into your app. Each call is a durable step that runs with your credentials.
Experimental.
toolis exported frominngest/experimental. The API can change.
- Keep credentials out of the Sandbox. The tool runs in your function, so the Sandbox never sees your keys.
- Get durable calls. Every call is its own step: retried, memoized, and visible in the trace.
- Use your live scope. A tool can close over a client or tenant created earlier in the handler.
- Call anything. A plain function, an
Inngestfunction, or a group of steps.
Give a command a tool
Pass tools to commands.run. The code calls it by name.
await sandbox.commands.run("agent", "node agent.js", {
tools: {
getCustomer: (id: string) => db.customers.find(id),
},
});
Inside the Sandbox, import inngest_tools:
// agent.js
const { tools } = require("inngest_tools");
async function main() {
const customer = await tools.getCustomer("cus_1");
}
main();
# agent.py
import asyncio
from inngest_tools import tools
async def main():
customer = await tools.getCustomer("cus_1")
asyncio.run(main())
A tool that fails raises a ToolError the code can catch. Tools work with step.sandbox; the direct inngest.sandboxes client does not run them.
Use tools in a runner
step.sandbox.python and step.sandbox.typescript take tools too. The code calls them with no import.
const total = await step.sandbox.python(
"total",
`customer = await tools.getCustomer("cus_1")
return customer["seats"] * 10`,
{
tools: { getCustomer: (id: string) => db.customers.find(id) },
schema: { output: z.number() },
},
);
Type the arguments with tool.step()
A plain function takes positional arguments. Declare tool.step() for named, validated ones. A bad call gets a ToolError in the Sandbox and nothing runs.
import { tool } from "inngest/experimental";
import { z } from "zod";
const getCustomer = tool.step({
description: "Look up a customer by ID",
input: z.object({ id: z.string() }),
output: z.object({ name: z.string() }),
run: async ({ id }, { idempotencyKey }) => {
return db.customers.find(id);
},
});
await sandbox.commands.run("agent", "node agent.js", {
tools: { getCustomer },
});
const customer = await tools.getCustomer({ id: "cus_1" });
customer = await tools.getCustomer(id="cus_1")
inputandoutputtake any Standard Schema.idempotencyKeystays the same across retries. Pass it to APIs that deduplicate writes.
Run steps inside a tool with tool.group()
The body of a tool.group() can use real steps: run them in parallel, invoke other functions, send events, or wait for one.
import { step } from "inngest";
import { tool } from "inngest/experimental";
const refund = tool.group({
input: z.object({ orderId: z.string() }),
run: async ({ orderId }) => {
const result = await step.invoke("refund", {
function: refundFn,
data: { orderId },
});
await step.sendEvent("notify", { name: "order/refunded", data: result });
return result;
},
});
Pass an Inngest function
An Inngest function, or a referenceFunction() to one, is a tool too. The code passes named arguments, which become the event data, and gets the function's result back.
import { eventType } from "inngest";
const refund = inngest.createFunction(
{
id: "refund",
triggers: [
eventType("order/refund", {
schema: z.object({ orderId: z.string() }),
}),
],
},
async ({ event }) => issueRefund(event.data.orderId),
);
await sandbox.commands.run("agent", "node agent.js", {
tools: { refund },
});
The arguments are checked against the function's trigger schema when it has one. Without a schema, any named arguments are sent; wrap the invoke in tool.group() to declare the input you want.
Limits and options
toolOptions tunes how calls run:
await sandbox.commands.run("agent", "node agent.js", {
tools: { getCustomer },
toolOptions: { toolTimeout: "10s", maxCalls: 50 },
});
| Option | Default | What it does |
|---|---|---|
toolTimeout | 30 seconds | How long one tool.step() may run before its step fails. |
maxCalls | 100 | Tool calls allowed in one command before it is stopped. |
waitWindow | 20 seconds | The longest one wait step blocks before a fresh one continues. |
- Tool names start with a letter, then letters, digits, or
_, up to 64 characters. - A request from the Sandbox can be 256 KiB, and a reply 1 MiB.
- A command with tools can run for 5 minutes when you set no
timeout. - Arguments arrive from the Sandbox, so validate them with
tool.step()when the code is untrusted.
Give the tools to a model v4.26.0+
toolDefinitions(tools) (experimental, from inngest/experimental) returns { name, description, input_schema } for each tool, ready for a model API. No hand-written JSON Schema.
import { toolDefinitions } from "inngest/experimental";
await anthropic.messages.create({
model,
max_tokens: 1024,
messages,
tools: toolDefinitions({ getCustomer }),
});
- For OpenAI, map each definition to
{ type: "function", name, description, parameters: input_schema }. - Declare tools with
tool.step()ortool.group()and an objectinputschema. Schemas come from Zod 4 or any library that implements Standard JSON Schema; other schemas throw. - A tool without an
inputschema takes any object.