Skip to content

Diagnostics channels

Last updated View as MarkdownAgent setup

The Lifecycle and its capabilities publish structured events to diagnostics channels for significant operations, such as RPC calls, schedule execution, job queue warnings, and MCP connections. The Agent class publishes more events for state changes, chat, workflows, and email. Publishing has no overhead when nobody is listening.

Events work the same on any Durable Object that installs the Lifecycle and on the Agent class.

Event structure

Every event has these fields:

{
  type: "rpc",                        // what happened
  source: "websockets",               // which capability emitted it
  agent: "MyAgent",                   // which Durable Object class emitted it
  name: "user-123",                   // which instance (Durable Object name)
  payload: { method: "getWeather" },  // details
  timestamp: 1758005142787            // when (ms since epoch)
}
  • agent is the Durable Object class name and name is the instance name.
  • source is the ID of the capability that emitted the event, or lifecycle for events from the Lifecycle job queue. It is set on events that a Lifecycle publishes directly. Events emitted through the Agent class's observability property do not have a source.

Channels

Each event goes to one channel, chosen by the prefix of its type:

Channel Event types Description
agents:state state:* State sync events
agents:rpc rpc, rpc:* RPC method calls and failures
agents:message message:*, tool:*, submission:*, action:* Chat message, tool, Think submission, and Think action lifecycle
agents:chat chat:*, except chat:transcript:* Chat turns, requests, recovery, stream stalls, and context handling
agents:transcript transcript:*, chat:transcript:* Transcript repair events
agents:fiber fiber:* Durable fiber lifecycle
agents:agent_tool agent_tool:* Parent and child agent-tool runs
agents:schedule schedule:*, queue:* Scheduled and queued callbacks
agents:workflow workflow:* Workflow state transitions
agents:mcp mcp:* MCP client operations
agents:email email:* Email processing
agents:task task:* Events from the experimental Tasks capability. Not covered on this page
agents:stream stream:* Events from the experimental Streams capability. Not covered on this page
agents:channel channel:*, notice:* Events from the experimental channels module. Not covered on this page
agents:lifecycle connect, disconnect, destroy, job:*, alarm:*, and every type that matches no other prefix Connections, teardown, the job queue, and custom capability events

agents:lifecycle is the fallback channel. An event whose type matches none of the prefixes above, such as an event from your own capability, lands there.

Subscribing to events

Typed subscribe helper

The subscribe() function from agents/observability provides type-safe access to events on a specific channel:

import { subscribe } from "agents/observability";

const unsub = subscribe("rpc", (event) => {
	if (event.type === "rpc") {
		console.log(`RPC call: ${event.payload.method}`);
	}
	if (event.type === "rpc:error") {
		console.error(
			`RPC failed: ${event.payload.method} — ${event.payload.error}`,
		);
	}
});

// Clean up when done
unsub();
import { subscribe } from "agents/observability";

const unsub = subscribe("rpc", (event) => {
	if (event.type === "rpc") {
		console.log(`RPC call: ${event.payload.method}`);
	}
	if (event.type === "rpc:error") {
		console.error(
			`RPC failed: ${event.payload.method} — ${event.payload.error}`,
		);
	}
});

// Clean up when done
unsub();

The callback is typed: event is narrowed to the event types in the event reference for that channel. Events from your own capabilities are not part of those types. Read them with the raw API.

The typed helper uses camelCase keys, so agent-tool events are subscribe("agentTool", ...). Raw diagnostics channel subscribers use the emitted channel name, agents:agent_tool.

Raw diagnostics_channel

You can also subscribe directly using the Node.js API:

import { subscribe } from "node:diagnostics_channel";

subscribe("agents:schedule", (event) => {
	console.log(event);
});
import { subscribe } from "node:diagnostics_channel";

subscribe("agents:schedule", (event) => {
	console.log(event);
});

Emit events from your own capability

A capability that extends LifecycleCapability publishes events with this.lifecycle.events.emit(type, payload). The Lifecycle adds the source, agent, name, and timestamp fields and routes the event by its type prefix. A type with an unknown prefix lands on agents:lifecycle:

src/index.jsjs
import { DurableObject } from "cloudflare:workers";
import { Lifecycle, LifecycleCapability } from "agents/lifecycle";

class Orders extends LifecycleCapability {
	constructor() {
		super("orders");
	}

	place(orderId) {
		// Unknown prefix, so this lands on agents:lifecycle
		this.lifecycle.events.emit("order:placed", { orderId });
	}
}

export class Shop extends DurableObject {
	orders = new Orders();
	lifecycle = Lifecycle.install(this).use(this.orders);
}
src/index.tsts
import { DurableObject } from "cloudflare:workers";
import { Lifecycle, LifecycleCapability } from "agents/lifecycle";

class Orders extends LifecycleCapability {
	constructor() {
		super("orders");
	}

	place(orderId: string) {
		// Unknown prefix, so this lands on agents:lifecycle
		this.lifecycle.events.emit("order:placed", { orderId });
	}
}

export class Shop extends DurableObject<Env> {
	readonly orders = new Orders();
	readonly lifecycle = Lifecycle.install(this).use(this.orders);
}

Read these events by type on the raw channel:

import { subscribe } from "node:diagnostics_channel";

subscribe("agents:lifecycle", (message) => {
	const event = message;
	if (event.type === "order:placed") {
		console.log(event.source, event.payload);
	}
});
import { subscribe } from "node:diagnostics_channel";

subscribe("agents:lifecycle", (message) => {
	const event = message as { type: string; source?: string; payload: unknown };
	if (event.type === "order:placed") {
		console.log(event.source, event.payload);
	}
});

Events are best effort. Events emitted while the Lifecycle is starting are held and published once startup finishes, and dropped if startup fails. Refer to Build a capability for the rest of the capability API.

Tail Workers (production)

In production, all diagnostics channel messages are automatically forwarded to Tail Workers. No subscription code is needed in the agent itself. Attach a Tail Worker and read events from event.diagnosticsChannelEvents:

export default {
	async tail(events) {
		for (const event of events) {
			for (const msg of event.diagnosticsChannelEvents) {
				// msg.channel is "agents:rpc", "agents:lifecycle", etc.
				// msg.message is the event
				console.log(msg.timestamp, msg.channel, msg.message);
			}
		}
	},
};
export default {
	async tail(events) {
		for (const event of events) {
			for (const msg of event.diagnosticsChannelEvents) {
				// msg.channel is "agents:rpc", "agents:lifecycle", etc.
				// msg.message is the event
				console.log(msg.timestamp, msg.channel, msg.message);
			}
		}
	},
};

This gives you structured, filterable observability in production with no overhead in the agent hot path.

On the Agent class

The Agent class sends its events through an observability property. The default implementation, genericObservability from agents/observability, publishes each event to the channels above. A Durable Object that installs the Lifecycle directly has no observability property. Its events always go to the channels, and you filter them in the subscriber.

To replace the default on an Agent, provide your own Observability implementation:

import { Agent } from "agents";

const myObservability = {
	emit(event) {
		// Send to your logging service, filter events, etc.
		if (event.type === "rpc:error") {
			console.error(event.payload.method, event.payload.error);
		}
	},
};

class MyAgent extends Agent {
	observability = myObservability;
}
import { Agent } from "agents";
import type { Observability } from "agents/observability";

const myObservability: Observability = {
	emit(event) {
		// Send to your logging service, filter events, etc.
		if (event.type === "rpc:error") {
			console.error(event.payload.method, event.payload.error);
		}
	},
};

class MyAgent extends Agent {
	override observability = myObservability;
}

Set observability to undefined to disable all event emission:

import { Agent } from "agents";

class MyAgent extends Agent {
	observability = undefined;
}
import { Agent } from "agents";

class MyAgent extends Agent {
	override observability = undefined;
}

Event reference

RPC events

Type Payload When
rpc { method, streaming? } A client calls a method on the WebSockets capability's callables, or a @callable method
rpc:error { method, error } That method throws

State events

Type Payload When
state:update {} setState() is called on an Agent

Message, tool, and submission events

These events track chat message lifecycle, client-side tool interactions, and Think durable submissions.

Type Payload When
message:request {} A chat message is received
message:response {} A chat response stream completes
message:clear {} Chat history is cleared
message:cancel { requestId } A streaming request is cancelled
message:error { error } A chat stream fails
tool:result { toolCallId, toolName } A client tool result is received
tool:approval { toolCallId, approved } A tool call is approved or rejected
submission:create { submissionId } A Think submission is accepted
submission:status { submissionId, status } A Think submission status changes
submission:error { submissionId, error } A Think submission fails

Chat turn events

Type Payload When
chat:turn:start { requestId, trigger, admission, continuation?, generation? } A chat turn starts
chat:turn:finish { requestId, trigger, admission, continuation?, generation?, status, durationMs, error? } A chat turn ends

Chat recovery events

Type Payload When
chat:request:failed { requestId?, stage, messagesPersisted?, error } A Think chat request fails while parsing, persisting, running, or streaming
chat:recovery:detected { incidentId, requestId, attempt, maxAttempts, recoveryKind } An interrupted chat fiber is first observed
chat:recovery:attempt { incidentId, requestId, attempt, maxAttempts, recoveryKind } The framework begins a recovery attempt
chat:recovery:scheduled { incidentId, requestId, attempt, maxAttempts, recoveryKind } A retry or continuation callback is scheduled
chat:recovery:completed { incidentId, requestId, attempt, maxAttempts, recoveryKind } Recovery completed successfully
chat:recovery:skipped { incidentId, requestId, attempt, maxAttempts, recoveryKind, reason? } Recovery was skipped because the conversation changed or was no longer recoverable
chat:recovery:failed { incidentId, requestId, attempt, maxAttempts, recoveryKind, reason? } Recovery ran but failed
chat:recovery:exhausted { incidentId, requestId, attempt, maxAttempts, recoveryKind, reason } Recovery exceeded its configured attempt budget
chat:stream:stalled { requestId, timeoutMs } The inactivity watchdog fired because no stream chunk arrived within chatStreamStallTimeoutMs. The turn routes into durable recovery

recoveryKind is "retry" when recovery replays an unanswered user turn and "continue" when it continues a partial assistant turn.

Chat context events

Type Payload When
chat:context:compacted { reason, shortened, requestId?, attempt? } Think compacts the session to handle a context-window overflow. reason is "proactive" (the contextOverflow.proactive guard fired before a step) or "reactive" (contextOverflow.reactive fired after an overflow). shortened is whether compaction actually reduced history — false means a retry would overflow again. Refer to Context-window overflow recovery.

Transcript events

Type Payload When
chat:transcript:repaired { requestId?, removedToolCalls, normalizedInputs, toolCallIds? } Think repairs a persisted transcript before sending it to the provider. removedToolCalls counts orphaned tool calls healed; normalizedInputs counts stringified or missing tool inputs repaired

Fiber events

Type Payload When
fiber:run:started { fiberId, fiberName, managed? } A durable fiber starts
fiber:run:completed { fiberId, fiberName, managed?, elapsedMs? } A durable fiber completes
fiber:run:failed { fiberId, fiberName, managed?, error, elapsedMs? } A durable fiber throws
fiber:run:interrupted { fiberId, fiberName, managed?, recoveryReason, elapsedMs? } Startup finds an interrupted fiber
fiber:recovery:detected { fiberId, fiberName, managed?, recoveryReason, elapsedMs? } Recovery sees an interrupted fiber
fiber:recovery:attempt { fiberId, fiberName, managed?, recoveryReason } A recovery hook starts
fiber:recovery:handled { fiberId, fiberName, managed?, recoveryReason, status, elapsedMs? } Recovery handling completes
fiber:recovery:skipped { fiberId, fiberName, managed?, reason, elapsedMs? } A recovery scan skips remaining work
fiber:recovery:failed { fiberId, fiberName, managed?, error, reason?, elapsedMs? } A recovery hook fails

Agent-tool recovery events

Type Payload When
agent_tool:recovery:begin { runCount, totalTimeoutMs? } Parent recovery starts scanning stale agent-tool runs
agent_tool:recovery:row { runId, agentType, status, reason?, elapsedMs? } One stale run is reconciled
agent_tool:recovery:deadline { runId, agentType, elapsedMs? } Total recovery deadline is exhausted before inspecting a row
agent_tool:recovery:complete { runCount, elapsedMs? } Parent recovery finishes scanning rows
agent_tool:recovery:failed { error } Parent recovery fails unexpectedly

Schedule and queue events

Type Payload When
schedule:create { callback, id } A schedule is created
schedule:execute { callback, id } A scheduled callback starts
schedule:cancel { callback, id } A schedule is cancelled
schedule:retry { callback, id, attempt, maxAttempts } A scheduled callback is retried
schedule:error { callback, id, error, attempts } A scheduled callback fails after all retries
schedule:duplicate_warning { callback, count, type } A non-idempotent schedule may duplicate work
queue:create { callback, id } A task is enqueued
queue:retry { callback, id, attempt, maxAttempts } A queued callback is retried
queue:error { callback, id, error, attempts } A queued callback fails after all retries

Lifecycle events

These events come from the Agent class:

Type Payload When
connect { connectionId } A WebSocket connection is established
disconnect { connectionId, code, reason } A WebSocket connection is closed
destroy {} The agent is destroyed

These events come from the Lifecycle job queue, on any Durable Object that installs the Lifecycle. Their source is lifecycle:

Type Payload When
job:slow_dispatch { capability, fn, id, thresholdMs } A job's onJob dispatch has run longer than its threshold. Long dispatches delay every other job on the object
job:backlog_warning { capability, count } One capability has many due jobs in a single alarm cycle, usually because one-shot jobs are pushed without a stable ID
alarm:memory_limit_reset { strikes, limit, sealed, error } An alarm hit the memory limit. sealed is true once the strike limit is reached

Workflow events

Type Payload When
workflow:start { workflowId, workflowName? } A workflow instance is started
workflow:event { workflowId, eventType? } An event is sent to a workflow
workflow:approved { workflowId, reason? } A workflow is approved
workflow:rejected { workflowId, reason? } A workflow is rejected
workflow:terminated { workflowId, workflowName? } A workflow is terminated
workflow:paused { workflowId, workflowName? } A workflow is paused
workflow:resumed { workflowId, workflowName? } A workflow is resumed
workflow:restarted { workflowId, workflowName? } A workflow is restarted

MCP events

Type Payload When
mcp:client:preconnect { serverId } Before connecting to an MCP server
mcp:client:connect { url, transport, state, error? } An MCP connection attempt completes or fails
mcp:client:authorize { serverId, authUrl, clientId? } An MCP OAuth flow begins
mcp:client:discover { url?, state?, error?, capability? } MCP capability discovery succeeds or fails
mcp:client:close { url, transport?, state, error?, phase? } An MCP connection is closed

Email events

Type Payload When
email:receive { from, to, subject? } An email is received
email:reply { from, to, subject? } A reply email is sent
email:send { from, to, subject? } An email is sent

Next steps

Tracing

Trace model calls, tool runs, and approvals with Workers traces.

Tail Workers

Forward diagnostics channel events to a Tail Worker for production monitoring.

Was this helpful?