---
title: Events
---

Full catalog of lifecycle and runtime events emitted by the Routecraft context. {% .lead %}

## Event payload

All events share the same envelope:

```ts
{
  ts: string       // ISO timestamp
  context: CraftContext
  details: {...}   // event-specific fields (see tables below)
}
```

## Context events

| Event | When it fires | Details |
| --- | --- | --- |
| `context:starting` | Before the context starts | `{}` |
| `context:started` | After all capabilities have started | `{}` |
| `context:stopping` | Before shutdown begins | `{ reason? }` |
| `context:stopped` | After all capabilities have stopped | `{}` |

## Route events

"Route" here refers to a registered capability internally.

| Event | When it fires | Details |
| --- | --- | --- |
| `route:registered` | Capability registered with the context | `{ route }` |
| `route:starting` | Just before a capability starts | `{ route }` |
| `route:started` | Capability is running | `{ route }` |
| `route:stopping` | Capability is stopping | `{ route, reason?, exchange? }` |
| `route:stopped` | Capability has stopped | `{ route, exchange? }` |

## Exchange events

Fired per exchange, scoped to the capability that owns it. `routeId` is the capability ID.

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:exchange:started` | Exchange enters the pipeline (parent or child) | `{ routeId, exchangeId, correlationId }` |
| `route:{routeId}:exchange:completed` | Exchange finished successfully (or consumed by aggregate) | `{ routeId, exchangeId, correlationId, duration }` |
| `route:{routeId}:exchange:failed` | Exchange encountered an unrecoverable error | `{ routeId, exchangeId, correlationId, duration, error }` |
| `route:{routeId}:exchange:dropped` | Exchange intentionally removed from the pipeline | `{ routeId, exchangeId, correlationId, reason }` |
| `route:{routeId}:exchange:restored` | Exchange restored from cache, skipping steps | `{ routeId, exchangeId, correlationId, source }` |

The `exchangeId` field is the exchange's own ID, not the correlation ID. Use `correlationId` to group related exchanges (e.g. a parent and its split children share the same correlation ID).

**Lifecycle guarantee:** every `exchange:started` is eventually followed by exactly one of `completed`, `failed`, or `dropped`.

## Operation events

Operation events are scoped to a capability and an operation type. They fire for individual steps in the pipeline.

### Adapter operations

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:operation:from:{adapterId}:started` | Source adapter activated | `{ routeId, exchangeId, correlationId, operation, adapterId, metadata? }` |
| `route:{routeId}:operation:from:{adapterId}:stopped` | Source adapter completed | `{ routeId, exchangeId, correlationId, operation, adapterId, duration, metadata? }` |
| `route:{routeId}:operation:to:{adapterId}:started` | Destination adapter invoked | `{ routeId, exchangeId, correlationId, operation, adapterId, metadata? }` |
| `route:{routeId}:operation:to:{adapterId}:stopped` | Destination adapter completed | `{ routeId, exchangeId, correlationId, operation, adapterId, duration, metadata? }` |

The `metadata` field is populated by the adapter's `getMetadata()` method. For example, the HTTP adapter returns `{ method, url, statusCode, contentLength }`.

### Batch operations

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:operation:batch:started` | Batch accumulation started | `{ routeId, batchId, batchSize }` |
| `route:{routeId}:operation:batch:flushed` | Batch released for processing | `{ routeId, batchId, batchSize, waitTime, reason }` |
| `route:{routeId}:operation:batch:stopped` | Batch accumulation stopped | `{ routeId, batchId }` |

`reason` is `'size'` when the batch hit its size limit, `'time'` when the flush interval elapsed.

### Split and aggregate

Split and aggregate use standard `step:started`/`step:completed` events (not dedicated operation events). Operation-specific data is in the `metadata` field:

- **Split** `step:completed` includes `metadata.childCount`: the number of child exchanges created
- **Aggregate** `step:completed` includes `metadata.inputCount`: the number of exchanges merged

After a split, each child exchange emits its own `exchange:started`. When aggregate consumes children, it emits `exchange:completed` for each child before continuing on the parent exchange.

### Retry operations

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:operation:retry:started` | Retry sequence started | `{ routeId, exchangeId, correlationId, maxAttempts }` |
| `route:{routeId}:operation:retry:attempt` | One retry attempt made | `{ routeId, exchangeId, correlationId, attemptNumber, maxAttempts, backoffMs, lastError? }` |
| `route:{routeId}:operation:retry:stopped` | Retry sequence ended | `{ routeId, exchangeId, correlationId, attemptNumber, success }` |

### Choice operations

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:operation:choice:matched` | A `when` or `otherwise` branch matched | `{ routeId, exchangeId, correlationId, branchIndex, branchLabel }` |
| `route:{routeId}:operation:choice:unmatched` | No branch matched and the exchange is dropped | `{ routeId, exchangeId, correlationId }` |

`branchLabel` is `"when"` or `"otherwise"`. `branchIndex` is the zero-based index of the matched branch.

### Error handler operations

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:error-handler:invoked` | A `.error()` handler runs (route or step scope) | `{ routeId, exchangeId, correlationId, originalError, failedOperation, scope: "route" \| "step", stepLabel? }` |
| `route:{routeId}:error-handler:recovered` | Handler returned a value; pipeline continues (step scope) or replaces body (route scope) | Same plus `recoveryStrategy` |
| `route:{routeId}:error-handler:failed` | Handler itself threw; rethrows for the next layer (route scope or default error path) | Same |

`scope` is `"route"` for the catch-all set via `.error()` BEFORE `.from()`, and `"step"` for a wrapper attached AFTER `.from()`. `stepLabel` is the label of the wrapped step when `scope === "step"`. Wildcard subscribers (`route:*:error-handler:*`) keep matching.

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:operation:error:invoked` | Reserved for the planned `.onError()` operation | `{ routeId, exchangeId, correlationId }` |
| `route:{routeId}:operation:error:recovered` | Reserved for the planned `.onError()` operation | `{ routeId, exchangeId, correlationId }` |
| `route:{routeId}:operation:error:failed` | Reserved for the planned `.onError()` operation | `{ routeId, exchangeId, correlationId, error }` |

### Agent operations

Emitted by `agent()` destinations. These are the **coarse decision events**: broadcast to every subscriber, no opt-in needed. For token-level streaming use `AgentOptions.onDelta` instead (a separate per-call channel).

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:agent:tool:invoked` | Agent decided to call a tool (input validated, before guard) | `{ routeId, exchangeId, correlationId, toolCallId, toolName, input }` |
| `route:{routeId}:agent:tool:result` | Tool handler returned a value | `{ routeId, exchangeId, correlationId, toolCallId, toolName, output, duration }` |
| `route:{routeId}:agent:tool:error` | Tool handler / guard / input validation threw | `{ routeId, exchangeId, correlationId, toolCallId, toolName, error, duration }` |
| `route:{routeId}:agent:finished` | Agent dispatch returned a consolidated result | `{ routeId, exchangeId, correlationId, finishReason, inputTokens?, outputTokens?, totalTokens? }` |
| `route:{routeId}:agent:error` | Provider / transport error during dispatch | `{ routeId, exchangeId, correlationId, error }` |

Wildcard subscriptions (`route:*:agent:tool:*`, `route:*:agent:finished`) work for cross-cutting telemetry, dashboards, and TUIs.

```ts
ctx.on('route:*:agent:tool:invoked', ({ details }) => {
  log.info({ tool: details.toolName, input: details.input }, 'Agent called tool');
});

ctx.on('route:*:agent:finished', ({ details }) => {
  metrics.histogram('agent.tokens.total', details.totalTokens ?? 0);
});
```

### Source-parse operations

Parsing source adapters (`json`, `html`, `csv`, `jsonl`, `mail`) defer parsing
to a synthetic first pipeline step so parse failures become normal pipeline
events. The synthetic step appears in the standard `step:*` events with
`operation: "parse"`.

| Event | When it fires | Details |
| --- | --- | --- |
| `route:{routeId}:step:started` (`operation: "parse"`) | Synthetic parse step begins, before any user step | `{ routeId, exchangeId, correlationId, operation: "parse", adapter: "parse" }` |
| `route:{routeId}:step:completed` (`operation: "parse"`) | Parse succeeded; user steps run next | `{ ..., duration }` |
| `route:{routeId}:step:failed` (`operation: "parse"`) | Parse threw `RC5016` | `{ ..., error }` |

What follows depends on the adapter's `onParseError` mode:

- `'fail'` (default) → `exchange:failed` (or `error:caught` if a route `.error()` handler recovers).
- `'abort'` → `exchange:failed` for the bad item, then the source aborts and `context:error` fires.
- `'drop'` → `exchange:dropped` with `reason: "parse-failed"` (no `step:failed` fires; the parse step catches and drops cleanly).

Subscribe with a glob to count source parse failures across all routes:

```ts
ctx.on('route:*:step:failed', ({ details }) => {
  if (details.operation === 'parse') metrics.increment('source.parse.failed');
});
ctx.on('route:*:exchange:dropped', ({ details }) => {
  if (details.reason === 'parse-failed') metrics.increment('source.parse.dropped');
});
```

## Plugin events

Plugin events are scoped to a plugin ID.

| Event | When it fires | Details |
| --- | --- | --- |
| `plugin:{pluginId}:registered` | Plugin registered | `{ pluginId, pluginIndex }` |
| `plugin:{pluginId}:starting` | Plugin is about to start | `{ pluginId, pluginIndex }` |
| `plugin:{pluginId}:started` | Plugin has started | `{ pluginId, pluginIndex }` |
| `plugin:{pluginId}:stopping` | Plugin is about to stop | `{ pluginId, pluginIndex }` |
| `plugin:{pluginId}:stopped` | Plugin has stopped | `{ pluginId, pluginIndex }` |

## Authentication events

Emitted by auth-enabled adapters (currently MCP HTTP) on every auth attempt. The `source` field identifies which adapter emitted the event.

| Event | When it fires | Details |
| --- | --- | --- |
| `auth:success` | Token validated and principal resolved | `{ subject, scheme, source }` |
| `auth:rejected` | Auth failed (missing header, bad scheme, or invalid token) | `{ reason, scheme, source }` |

`reason` is one of `"missing_header"`, `"unsupported_scheme"`, or `"invalid_token"`.

## MCP plugin events

Events emitted by the MCP plugin during server and tool lifecycle. Subscribe with wildcards (e.g. `plugin:mcp:tool:**`) for broad observability.

### Server events

| Event | When it fires | Details |
| --- | --- | --- |
| `plugin:mcp:server:listening` | HTTP server is ready to accept connections | `{ host, port, path }` |
| `plugin:mcp:server:tools:exposed` | Tool list logged for the first time | `{ tools, count }` |

### Session events

| Event | When it fires | Details |
| --- | --- | --- |
| `plugin:mcp:session:created` | New HTTP client session initialized | `{ sessionId }` |
| `plugin:mcp:session:closed` | HTTP client session transport closed | `{ sessionId }` |

### Tool call events

| Event | When it fires | Details |
| --- | --- | --- |
| `plugin:mcp:tool:called` | Tool invocation started | `{ tool, args }` |
| `plugin:mcp:tool:completed` | Tool invocation succeeded | `{ tool }` |
| `plugin:mcp:tool:failed` | Tool invocation failed | `{ tool, error }` |

---

## Related

{% quick-links %}

{% quick-link title="Events" icon="theming" href="/docs/introduction/events" description="How to subscribe, use wildcards, emit custom events, and common patterns." /%}
{% quick-link title="Configuration" icon="presets" href="/docs/reference/configuration" description="Subscribe to events via craft.config.ts." /%}

{% /quick-links %}
