# Defining and Registering Custom Tools for LLM Agents Using the ToolDef Interface

> Learn how to define and register custom tools for LLM agents using Flue's ToolDef interface. Easily integrate deterministic functions with JSON Schema parameters and async logic into your agent's workflow.

- Repository: [Astro/flue](https://github.com/withastro/flue)
- Tags: tutorial
- Published: 2026-05-11

---

**Flue provides a `ToolDef` interface that lets you register deterministic functions—complete with JSON Schema parameters and async execution logic—that LLM agents can invoke during conversations by passing them to `init()` for global availability or to individual `prompt`, `skill`, or `task` calls for scoped use.**

The withastro/flue SDK enables you to extend LLM agent capabilities beyond built-in operations by implementing the **`ToolDef`** interface. This pattern allows you to expose any deterministic JavaScript or TypeScript function as a callable tool that the model can invoke during reasoning. Understanding how to define, validate, and register these tools is essential for building specialized agent workflows.

## Understanding the ToolDef Interface Structure

The contract for custom tools is defined in **[`packages/sdk/src/types.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts)** (line 57). To create a valid tool, you must provide four properties:

- **`name`** – A unique string identifier that cannot collide with built-in tools like `task` or `shell`.
- **`description`** – Natural language guidance that helps the LLM decide when to invoke the tool.
- **`parameters`** – A JSON Schema object describing expected arguments. Flue provides a **`Type`** helper from `@flue/sdk/client` to build these schemas fluently.
- **`execute`** – An async function receiving parsed arguments and an optional **`AbortSignal`**, returning a string result. Thrown errors surface to the model as tool errors.

## Tool Validation and Lifecycle

Before execution, Flue validates custom tools in the **`Session.createCustomTools`** method located in **[`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts)** (line 13). This validation ensures that no custom tool name conflicts with **`BUILTIN_TOOL_NAMES`** and that all names are unique within the custom tool set.

Upon validation, each `ToolDef` converts into an internal **`AgentTool`** object. This wrapper structures the return value as `{ content: [{ type: 'text', text: <result> }], details: { customTool: <tool-name> } }` and handles `AbortSignal` propagation for cancellation support.

## Registration Patterns

Flue supports two distinct strategies for **defining and registering custom tools** depending on your scope requirements.

### Global Registration via init()

To make tools available for every session interaction, pass them to the **`init`** function defined in **[`packages/sdk/src/harness.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/harness.ts)**. This registers them globally across all subsequent calls:

```typescript
import { init, Type, type ToolDef } from '@flue/sdk/client';

const calculator: ToolDef = {
  name: 'calculator',
  description: 'Perform arithmetic and return the numeric result.',
  parameters: Type.Object({
    expression: Type.String({ description: 'Math expression like "2 + 3"' }),
  }),
  async execute(args) {
    const result = Function(`"use strict"; return (${args.expression})`)();
    return String(result);
  },
};

const harness = await init({
  sandbox,
  model: 'anthropic/claude-sonnet-4-6',
  tools: [calculator], // Available to all subsequent calls
});

```

### Per-Call Registration

For scoped tool access, pass the `tools` array to individual methods like **`prompt`**, **`skill`**, or **`task`**. This limits tool availability to that specific invocation:

```typescript
const { text } = await session.prompt(
  'Calculate 7 * 6 using the calculator tool.',
  { tools: [calculator] }, // Only available for this specific call
);

```

## Execution Flow and Error Handling

When an LLM decides to invoke a tool, the following sequence occurs:

1. The model emits a tool-call payload containing the tool name and arguments.
2. Flue matches the name against registered **`AgentTool`** instances.
3. The wrapped **`execute`** function runs with parsed JSON arguments.
4. The string result returns to the LLM as a tool result message.
5. If execution throws, Flue surfaces the error as a tool-error message visible to the model.

**Built-in tools** (such as `task` and `shell`) always register first, followed by custom tools in the order provided.

## Complete Working Example

The following example from **[`examples/hello-world/.flue/agents/with-tools.ts`](https://github.com/withastro/flue/blob/main/examples/hello-world/.flue/agents/with-tools.ts)** demonstrates the full lifecycle—from definition to delegation.

**Defining the tool** (lines 24-36):

```typescript
import { Type, type ToolDef } from '@flue/sdk/client';

const calculator: ToolDef = {
  name: 'calculator',
  description: 'Perform arithmetic. Returns the numeric result as a string.',
  parameters: Type.Object({
    expression: Type.String({
      description: 'A math expression like "2 + 3"',
    }),
  }),
  async execute(args) {
    const expr = args.expression as string;
    const result = Function(`"use strict"; return (${expr})`)();
    return String(result);
  },
};

```

**Per-call usage** (lines 38-42):

```typescript
const { text } = await session.prompt(
  'Use the calculator tool to compute 7 * 6. Tell me the result.',
  { tools: [calculator] },
);
console.log(text); // Output includes "42"

```

**Using tools in delegated tasks** (lines 45-57):

```typescript
await session.shell('mkdir -p /tmp/task-workspace');
await session.shell(
  'echo "You are a math helper. Always respond with just the numeric answer." > /tmp/task-workspace/AGENTS.md',
);

const taskResponse = await session.prompt(
  'Use the task tool with cwd /tmp/task-workspace to ask: "What is 100 + 23?"',
);
console.log(taskResponse.text); // → "123"

```

## Summary

- The **`ToolDef`** interface in **[`packages/sdk/src/types.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/types.ts)** requires `name`, `description`, `parameters` (JSON Schema), and an async `execute` function.
- Flue validates custom tools in **`Session.createCustomTools`** to prevent naming collisions with built-ins and ensure uniqueness.
- Register tools **globally** via `init({ tools })` in **[`packages/sdk/src/harness.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/harness.ts)** or **per-call** via the `tools` option on specific methods.
- The execution wrapper formats results as structured content objects and respects `AbortSignal` for cancellation.
- Custom tools coexist with built-in tools, extending agent capabilities without replacing core functionality.

## Frequently Asked Questions

### Can custom tools override Flue's built-in tools like task or shell?

No. The validation logic in **[`packages/sdk/src/session.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/session.ts)** explicitly checks custom tool names against **`BUILTIN_TOOL_NAMES`**. If a collision is detected, Flue throws a validation error before the session begins, ensuring built-in tools remain accessible.

### How does Flue handle errors thrown inside a tool's execute function?

When the **`execute`** function throws an error, Flue catches it and formats the result as a tool-error message that is visible to the LLM. This allows the model to potentially retry with different parameters or report the failure to the user, maintaining conversational continuity.

### What is the difference between the ToolDef interface and the internal AgentTool?

**`ToolDef`** is the public SDK interface you implement when defining custom functionality. **`AgentTool`** is Flue's internal representation created by `Session.createCustomTools` that wraps your `execute` method, handles JSON argument parsing, manages `AbortSignal` propagation, and formats the return value according to the expected conversation protocol.

### Can I register tools after initializing a session?

No. Tools must be registered either during the **`init`** call for global availability or at the time of specific method invocations (`prompt`, `skill`, `task`). There is no dynamic registration API for modifying active tool sets mid-session.