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. tool is exported from inngest/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 Inngest function, 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")
  • input and output take any Standard Schema.
  • idempotencyKey stays 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 },
});
OptionDefaultWhat it does
toolTimeout30 secondsHow long one tool.step() may run before its step fails.
maxCalls100Tool calls allowed in one command before it is stopped.
waitWindow20 secondsThe 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() or tool.group() and an object input schema. Schemas come from Zod 4 or any library that implements Standard JSON Schema; other schemas throw.
  • A tool without an input schema takes any object.