# defineRouter

> Define routing rules to direct conversations to the right agent

The `defineRouter` function creates routing rules that direct conversations to the right agent on a shared channel. Each router is defined in its own file under the `routers/` directory.

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

export default defineRouter({
  name: "Support Router",
  slug: "support-router",
  description: "Routes incoming WhatsApp messages to the right team",
  mode: "rules",
  agents: [
    { slug: "sales-agent", description: "Handles new leads and pricing questions" },
    { slug: "support-agent", description: "Handles technical issues and bug reports" },
  ],
  rules: [
    {
      conditions: [
        { field: "contact.type", operator: "eq", value: "lead" },
      ],
      route: "sales-agent",
    },
    {
      conditions: [
        { field: "contact.type", operator: "eq", value: "customer" },
      ],
      route: "support-agent",
    },
  ],
  fallback: "support-agent",
})
```

## RouterConfig

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | `string` | Yes | Display name for the router |
| `slug` | `string` | Yes | Unique identifier for the router |
| `description` | `string` | No | Human-readable description of the routing logic |
| `mode` | `'rules' \| 'classify'` | Yes | Routing strategy: condition-based or LLM classification |
| `agents` | `RouterAgentRef[]` | Yes | List of agents this router can route to (at least one required) |
| `rules` | `RouterRule[]` | When mode is `rules` | Ordered list of routing rules evaluated top-to-bottom |
| `fallback` | `string` | Yes | Agent slug to use when no rule matches or classification is uncertain |
| `classifyModel` | `{ model: string; temperature?: number; maxTokens?: number }` | When mode is `classify` | Model config in OpenRouter format for intent classification |
| `contextMessages` | `number` | No | Number of recent messages to include for classification context (default: 5) |
| `maxTransfers` | `number` | No | Maximum number of agent-to-agent transfers per conversation (default: 5) |
| `inactivityResetMs` | `number` | No | Milliseconds of inactivity before sticky routing resets |

### Validation

`defineRouter` throws errors if:

- `name` is missing
- `slug` is missing
- `agents` is missing or empty
- `fallback` does not reference a slug in `agents`
- `mode` is `rules` and `rules` is empty or missing
- Any rule has an empty `conditions` array
- Any rule's `route` does not reference a slug in `agents`
- Any condition is missing a `field`
- Any condition has an invalid `operator` (must be one of: `eq`, `neq`, `in`, `contains`, `regex`, `gt`, `lt`, `exists`)

### router.transfer

When a thread is created via a router, the `router.transfer` tool is automatically injected into each agent's tool list. Agents can call `router.transfer` to hand off the conversation to another agent within the same router.

```typescript
router.transfer({
  targetAgentSlug: "billing-agent",
  reason: "Customer is asking about their invoice"
})
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `targetAgentSlug` | `string` | Yes | Slug of the agent to transfer to (must be in the router's `agents` list) |
| `reason` | `string` | No | Explanation of why the transfer is happening |

Calling `router.transfer` on a thread that was not created via a router throws an error.

## RouterAgentRef

Each agent reference identifies an agent the router can route to.

```typescript
interface RouterAgentRef {
  slug: string
  description: string
}
```

| Field | Type | Description |
|-------|------|-------------|
| `slug` | `string` | Slug of the agent to route to |
| `description` | `string` | Description used by LLM classification to understand what this agent handles |

## RouterRule

Each rule defines a set of conditions and a target agent.

```typescript
interface RouterRule {
  conditions: RouterRuleCondition[]
  route: string
}
```

| Field | Type | Description |
|-------|------|-------------|
| `conditions` | `RouterRuleCondition[]` | All conditions must match for the rule to activate (AND logic) |
| `route` | `string` | Slug of the agent to route to when all conditions match |

Rules are evaluated top-to-bottom. The first matching rule wins. If no rule matches, the `fallback` agent handles the conversation.

## RouterRuleCondition

Each condition evaluates a field against a value using an operator.

```typescript
interface RouterRuleCondition {
  field: string
  operator: string
  value: unknown
}
```

| Field | Type | Description |
|-------|------|-------------|
| `field` | `string` | The field to evaluate (see available fields below) |
| `operator` | `string` | Comparison operator |
| `value` | `unknown` | Value to compare against |

### Available Fields

| Field | Type | Description |
|-------|------|-------------|
| `phoneNumber` | `string` | Sender's phone number |
| `channel` | `string` | Message channel (e.g., `whatsapp`, `api`) |
| `{entityType}.*` | `any` | Any field from an entity matched by phone number. The prefix is the entity type slug — `teacher.name` queries the `teacher` entity type, `contact.tier` queries `contact`, `student.grade` queries `student`. The routing engine extracts the entity type from the prefix and looks up the entity by the sender's phone number. |
| `time.hour` | `number` | Current hour (0-23) in the organization's timezone |
| `time.dayOfWeek` | `number` | Current day of week (0 = Sunday, 6 = Saturday) |
| `message.text` | `string` | Text content of the incoming message |
| `message.type` | `string` | Message type (e.g., `text`, `image`, `audio`) |

### Available Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `eq` | Equal to | `{ field: "channel", operator: "eq", value: "whatsapp" }` |
| `neq` | Not equal to | `{ field: "message.type", operator: "neq", value: "text" }` |
| `in` | Value is in array | `{ field: "customer.plan", operator: "in", value: ["pro", "enterprise"] }` |
| `contains` | String contains substring | `{ field: "message.text", operator: "contains", value: "pricing" }` |
| `regex` | Matches regular expression | `{ field: "phoneNumber", operator: "regex", value: "^\\+44" }` |
| `gt` | Greater than | `{ field: "time.hour", operator: "gt", value: 17 }` |
| `lt` | Less than | `{ field: "time.hour", operator: "lt", value: 9 }` |
| `exists` | Field exists and is not null | `{ field: "customer.assignedAgent", operator: "exists", value: true }` |

## Routing Modes

### Rules Mode

Rules mode evaluates conditions with zero LLM cost. Each rule is checked top-to-bottom until a match is found:

```typescript
export default defineRouter({
  name: "Hours Router",
  slug: "hours-router",
  mode: "rules",
  agents: [
    { slug: "live-agent", description: "Live support during business hours" },
    { slug: "after-hours-agent", description: "Automated support outside business hours" },
  ],
  rules: [
    {
      conditions: [
        { field: "time.hour", operator: "gt", value: 8 },
        { field: "time.hour", operator: "lt", value: 18 },
        { field: "time.dayOfWeek", operator: "gt", value: 0 },
        { field: "time.dayOfWeek", operator: "lt", value: 6 },
      ],
      route: "live-agent",
    },
  ],
  fallback: "after-hours-agent",
})
```

Multiple conditions within a rule use AND logic. The first matching rule wins.

### Classify Mode

Classify mode uses an LLM to determine intent from the conversation context. The LLM receives the agent descriptions and recent messages, then selects the best agent:

```typescript
export default defineRouter({
  name: "Intent Router",
  slug: "intent-router",
  mode: "classify",
  agents: [
    { slug: "billing-agent", description: "Handles invoices, payments, refunds, and subscription changes" },
    { slug: "technical-agent", description: "Handles bug reports, API questions, and technical troubleshooting" },
    { slug: "onboarding-agent", description: "Handles account setup, getting started, and feature walkthroughs" },
  ],
  classifyModel: { model: "openai/gpt-5-mini" },
  contextMessages: 5,
  fallback: "technical-agent",
})
```

The `description` field on each `RouterAgentRef` is critical in classify mode — it provides the LLM with the context to make accurate routing decisions.

## Full Examples

### Route by Phone Number

Route internal team messages to a different agent than customer messages:

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

export default defineRouter({
  name: "Team Router",
  slug: "team-router",
  mode: "rules",
  agents: [
    { slug: "internal-agent", description: "Internal team assistant" },
    { slug: "customer-agent", description: "Customer-facing support agent" },
  ],
  rules: [
    {
      conditions: [
        { field: "phoneNumber", operator: "regex", value: "^\\+1555" },
      ],
      route: "internal-agent",
    },
  ],
  fallback: "customer-agent",
})
```

### Route by Contact Type

Route based on data stored on the contact entity:

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

export default defineRouter({
  name: "Contact Router",
  slug: "contact-router",
  mode: "rules",
  agents: [
    { slug: "vip-agent", description: "Dedicated support for enterprise customers" },
    { slug: "sales-agent", description: "Handles leads and trials" },
    { slug: "general-agent", description: "General support for all customers" },
  ],
  rules: [
    {
      conditions: [
        { field: "contact.plan", operator: "in", value: ["enterprise"] },
      ],
      route: "vip-agent",
    },
    {
      conditions: [
        { field: "contact.type", operator: "eq", value: "lead" },
      ],
      route: "sales-agent",
    },
  ],
  fallback: "general-agent",
})
```

### LLM Classification with Transfer Cap

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

export default defineRouter({
  name: "Smart Router",
  slug: "smart-router",
  mode: "classify",
  agents: [
    { slug: "billing-agent", description: "Invoices, payments, refunds, and plan changes" },
    { slug: "support-agent", description: "Technical issues, bugs, and how-to questions" },
    { slug: "sales-agent", description: "Pricing, demos, and new feature inquiries" },
  ],
  classifyModel: { model: "anthropic/claude-sonnet-4" },
  contextMessages: 3,
  maxTransfers: 3,
  inactivityResetMs: 1800000,
  fallback: "support-agent",
})
```
