# Tools&#x20;

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

```typescript {{ title: "TypeScript" }}
await sandbox.commands.run("agent", "node agent.js", {
  tools: {
    getCustomer: (id: string) => db.customers.find(id),
  },
});
```

Inside the Sandbox, import `inngest_tools`:

```js
// agent.js
const { tools } = require("inngest_tools");

async function main() {
  const customer = await tools.getCustomer("cus_1");
}

main();
```

```python
# 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`](/docs-markdown/sandboxes/features/managed-lifecycle#run-python-or-typescript-in-one-call) take `tools` too. The code calls them with no import.

```typescript {{ title: "TypeScript" }}
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.

```typescript {{ title: "TypeScript" }}
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 },
});
```

```js
const customer = await tools.getCustomer({ id: "cus_1" });
```

```python
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.

```typescript {{ title: "TypeScript" }}
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.

```typescript {{ title: "TypeScript" }}
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:

```typescript {{ title: "TypeScript" }}
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&#x20;

`toolDefinitions(tools)` (experimental, from `inngest/experimental`) returns `{ name, description, input_schema }` for each tool, ready for a model API. No hand-written JSON Schema.

```typescript {{ title: "TypeScript" }}
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.