# defineTools

> Create custom tool handlers for agents

The `defineTools` function creates and validates custom tool handlers. Custom tools are defined in `tools/index.ts` and synced to the platform as standalone org-level resources in the `customTools` table. Agents reference tools by name in their `tools` array — tools do not need to be embedded in agent definitions.

```typescript
import { defineTools } from 'struere'

export default defineTools([
  {
    name: "send_email",
    description: "Send an email to a recipient",
    parameters: {
      type: "object",
      properties: {
        to: { type: "string", description: "Recipient email address" },
        subject: { type: "string", description: "Email subject line" },
        body: { type: "string", description: "Email body content" },
      },
      required: ["to", "subject", "body"],
    },
    handler: async (args, context, struere, fetch) => {
      const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
        method: "POST",
        headers: {
          "Authorization": `Bearer ${process.env.SENDGRID_API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          personalizations: [{ to: [{ email: args.to }] }],
          from: { email: "noreply@example.com" },
          subject: args.subject,
          content: [{ type: "text/plain", value: args.body }],
        }),
      })
      return { success: response.ok }
    },
  },
])
```

## Tool Definition

Each tool in the array requires the following fields:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Unique tool identifier, referenced in agent `tools` arrays |
| `description` | `string` | Yes | Description shown to the LLM for tool selection |
| `parameters` | `ToolParameters` | Yes | JSON Schema defining the tool's input parameters |
| `handler` | `function` | Yes | Async function that executes when the tool is called |
| `templateOnly` | `boolean` | No | If true, tool is only available during system prompt template compilation and not exposed to the LLM at runtime. Defaults to `false`. |

### Validation

`defineTools` throws errors if any tool is missing `name`, `description`, `parameters`, or `handler`.

## Handler Function

The handler function receives four arguments:

```typescript
handler: async (args, context, struere, fetch) => {
  return { result: "value" }
}
```

| Argument | Type | Description |
|----------|------|-------------|
| `args` | `Record<string, unknown>` | Parsed arguments matching the `parameters` schema |
| `context` | `ToolContext` | Actor and organization context |
| `struere` | `StruereSDK` | SDK object providing access to all built-in tools |
| `fetch` | `function` | Native fetch function for making HTTP requests to any domain |

The handler must return a JSON-serializable value. This value is passed back to the LLM as the tool result.

### ToolContext

```typescript
interface ToolContext {
  organizationId?: string
  actorId?: string
  actorType?: string
  conversationId?: string
  userId?: string
}
```

| Field | Description |
|-------|-------------|
| `organizationId` | The Convex organization ID of the caller |
| `actorId` | The ID of the user or agent making the call |
| `actorType` | Whether the caller is a `"user"`, `"agent"`, or `"system"` |
| `conversationId` | The ID of the current conversation thread |
| `userId` | The ID of the end user in the conversation |

### StruereSDK

The `struere` parameter gives custom tool handlers access to all built-in tools. This enables custom tools to compose platform operations without making raw API calls.

```typescript
import type { StruereSDK } from 'struere'
```

Available namespaces:

| Namespace | Methods |
|-----------|---------|
| `struere.entity` | `create`, `get`, `query`, `update`, `delete` |
| `struere.event` | `emit`, `query` (**deprecated** — return `{ deprecated: true }`) |
| `struere.whatsapp` | `send`, `sendTemplate`, `sendInteractive`, `sendMedia`, `listTemplates`, `getConversation`, `getStatus` |
| `struere.calendar` | `list`, `create`, `update`, `delete`, `freeBusy` |
| `struere.airtable` | `listBases`, `listTables`, `listRecords`, `getRecord`, `createRecords`, `updateRecords`, `deleteRecords` |
| `struere.email` | `send` |
| `struere.payment` | `create`, `getStatus` |
| `struere.agent` | `chat` |
| `struere.web` | `search`, `fetch` |

Example using `struere` to create an entity inside a custom tool handler:

```typescript
export default defineTools([
  {
    name: "onboard_student",
    description: "Register a new student and emit an onboarding event",
    parameters: {
      type: "object",
      properties: {
        name: { type: "string" },
        email: { type: "string" },
      },
      required: ["name", "email"],
    },
    handler: async (args, context, struere) => {
      const student = await struere.entity.create({
        type: "student",
        data: { name: args.name, email: args.email },
      })
      await struere.event.emit({
        eventType: "student.onboarded",
        entityId: student.id,
      })
      return { studentId: student.id }
    },
  },
])
```

## Using Custom Tools in Agents

Reference custom tools by their `name` in any agent's `tools` array:

```typescript
import { defineAgent } from 'struere'

export default defineAgent({
  name: "Support Agent",
  slug: "support",
  version: "1.0.0",
  systemPrompt: "You are a customer support agent.",
  tools: [
    "entity.query",
    "entity.update",
    "event.emit",
    "send_email",
  ],
})
```

### Template-Only Tools

Tools defined with `templateOnly: true` are executed during system prompt template compilation but are not exposed to the LLM as callable tools at runtime. This lets you inject dynamic data into system prompts without bloating the agent's tool list.

Template-only tools are available to **all agents** in the organization automatically. They do not need to be listed in any agent's `tools` array to be used in system prompt templates.

```typescript
export default defineTools([
  {
    name: 'format_teacher_schedule',
    description: 'Query teachers and format availability into a readable schedule',
    templateOnly: true,
    parameters: {
      type: 'object',
      properties: {},
    },
    handler: async (args, context, struere) => {
      const result = await struere.entity.query({ type: 'teacher' })
      const teachers = Array.isArray(result) ? result : result?.results || []
      return teachers.map((t) => `${t.data?.name}: ${JSON.stringify(t.data?.availability)}`).join('\n')
    },
  },
])
```

Reference the tool in any agent's system prompt using template syntax:

```
{{format_teacher_schedule()}}
```

Template-only tools don't count toward the agent's runtime tool list.

Custom tool names and built-in tool names share the same namespace. Avoid naming conflicts by not using the `entity.`, `event.`, or `agent.` prefixes for custom tools.

For execution environment details and more examples, see [Custom Tools](../tools/custom-tools).
