Reference

Events

Full catalog of lifecycle and runtime events emitted by the Routecraft context.

Event payload

All events share the same envelope:

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

Context events

EventWhen it firesDetails
context:startingBefore the context starts{}
context:startedAfter all capabilities have started{}
context:stoppingBefore shutdown begins{ reason? }
context:stoppedAfter all capabilities have stopped{}

Route events

"Route" here refers to a registered capability internally.

EventWhen it firesDetails
route:registeredCapability registered with the context{ route }
route:startingJust before a capability starts{ route }
route:startedCapability is running{ route }
route:stoppingCapability is stopping{ route, reason?, exchange? }
route:stoppedCapability has stopped{ route, exchange? }

Exchange events

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

EventWhen it firesDetails
route:{routeId}:exchange:startedExchange enters the pipeline (parent or child){ routeId, exchangeId, correlationId }
route:{routeId}:exchange:completedExchange finished successfully (or consumed by aggregate){ routeId, exchangeId, correlationId, duration }
route:{routeId}:exchange:failedExchange encountered an unrecoverable error{ routeId, exchangeId, correlationId, duration, error }
route:{routeId}:exchange:droppedExchange intentionally removed from the pipeline{ routeId, exchangeId, correlationId, reason }
route:{routeId}:exchange:restoredExchange 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

EventWhen it firesDetails
route:{routeId}:operation:from:{adapterId}:startedSource adapter activated{ routeId, exchangeId, correlationId, operation, adapterId, metadata? }
route:{routeId}:operation:from:{adapterId}:stoppedSource adapter completed{ routeId, exchangeId, correlationId, operation, adapterId, duration, metadata? }
route:{routeId}:operation:to:{adapterId}:startedDestination adapter invoked{ routeId, exchangeId, correlationId, operation, adapterId, metadata? }
route:{routeId}:operation:to:{adapterId}:stoppedDestination 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

EventWhen it firesDetails
route:{routeId}:operation:batch:startedBatch accumulation started{ routeId, batchId, batchSize }
route:{routeId}:operation:batch:flushedBatch released for processing{ routeId, batchId, batchSize, waitTime, reason }
route:{routeId}:operation:batch:stoppedBatch 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

EventWhen it firesDetails
route:{routeId}:operation:retry:startedRetry sequence started{ routeId, exchangeId, correlationId, maxAttempts }
route:{routeId}:operation:retry:attemptOne retry attempt made{ routeId, exchangeId, correlationId, attemptNumber, maxAttempts, backoffMs, lastError? }
route:{routeId}:operation:retry:stoppedRetry sequence ended{ routeId, exchangeId, correlationId, attemptNumber, success }

Choice operations

EventWhen it firesDetails
route:{routeId}:operation:choice:matchedA when or otherwise branch matched{ routeId, exchangeId, correlationId, branchIndex, branchLabel }
route:{routeId}:operation:choice:unmatchedNo 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

EventWhen it firesDetails
route:{routeId}:error-handler:invokedA .error() handler runs (route or step scope){ routeId, exchangeId, correlationId, originalError, failedOperation, scope: "route" | "step", stepLabel? }
route:{routeId}:error-handler:recoveredHandler returned a value; pipeline continues (step scope) or replaces body (route scope)Same plus recoveryStrategy
route:{routeId}:error-handler:failedHandler 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.

EventWhen it firesDetails
route:{routeId}:operation:error:invokedReserved for the planned .onError() operation{ routeId, exchangeId, correlationId }
route:{routeId}:operation:error:recoveredReserved for the planned .onError() operation{ routeId, exchangeId, correlationId }
route:{routeId}:operation:error:failedReserved 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).

EventWhen it firesDetails
route:{routeId}:agent:tool:invokedAgent decided to call a tool (input validated, before guard){ routeId, exchangeId, correlationId, toolCallId, toolName, input }
route:{routeId}:agent:tool:resultTool handler returned a value{ routeId, exchangeId, correlationId, toolCallId, toolName, output, duration }
route:{routeId}:agent:tool:errorTool handler / guard / input validation threw{ routeId, exchangeId, correlationId, toolCallId, toolName, error, duration }
route:{routeId}:agent:finishedAgent dispatch returned a consolidated result{ routeId, exchangeId, correlationId, finishReason, inputTokens?, outputTokens?, totalTokens? }
route:{routeId}:agent:errorProvider / transport error during dispatch{ routeId, exchangeId, correlationId, error }

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

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

EventWhen it firesDetails
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:

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.

EventWhen it firesDetails
plugin:{pluginId}:registeredPlugin registered{ pluginId, pluginIndex }
plugin:{pluginId}:startingPlugin is about to start{ pluginId, pluginIndex }
plugin:{pluginId}:startedPlugin has started{ pluginId, pluginIndex }
plugin:{pluginId}:stoppingPlugin is about to stop{ pluginId, pluginIndex }
plugin:{pluginId}:stoppedPlugin 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.

EventWhen it firesDetails
auth:successToken validated and principal resolved{ subject, scheme, source }
auth:rejectedAuth 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

EventWhen it firesDetails
plugin:mcp:server:listeningHTTP server is ready to accept connections{ host, port, path }
plugin:mcp:server:tools:exposedTool list logged for the first time{ tools, count }

Session events

EventWhen it firesDetails
plugin:mcp:session:createdNew HTTP client session initialized{ sessionId }
plugin:mcp:session:closedHTTP client session transport closed{ sessionId }

Tool call events

EventWhen it firesDetails
plugin:mcp:tool:calledTool invocation started{ tool, args }
plugin:mcp:tool:completedTool invocation succeeded{ tool }
plugin:mcp:tool:failedTool invocation failed{ tool, error }

Events

How to subscribe, use wildcards, emit custom events, and common patterns.

Configuration

Subscribe to events via craft.config.ts.

Previous
Operations