---
description: Subscribe to structured agent events for RPC calls, state changes, schedules, workflows, and MCP connections.
title: Diagnostics channels
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt  
> Use this file to discover all available pages before exploring further.

# Diagnostics channels

Last updated Aug 20, 2026|Copy as Markdown| [View as Markdown](https://developers.cloudflare.com/agents/runtime/operations/observability/diagnostics-channels/index.md)| [Agent setup](https://developers.cloudflare.com/agent-setup/)

Agents publish structured events to [diagnostics channels](https://developers.cloudflare.com/workers/runtime-apis/nodejs/diagnostics-channel/) for every significant operation -- RPC calls, state changes, schedule execution, workflow transitions, MCP connections, and more. Publishing has zero overhead when nobody is listening.

## Event structure

Every event has these fields:

```ts
{
  type: "rpc",                        // what happened
  agent: "MyAgent",                   // which agent class emitted it
  name: "user-123",                   // which agent instance (Durable Object name)
  payload: { method: "getWeather" },  // details
  timestamp: 1758005142787            // when (ms since epoch)
}
```

`agent` and `name` identify the source agent — `agent` is the class name and `name` is the Durable Object instance name.

## Channels

Events are routed to named channels based on their type:

| Channel | Event types | Description |
| --- | --- | --- |
| `agents:state` | `state:update` | State sync events |
| `agents:rpc` | `rpc`, `rpc:error` | RPC method calls and failures |
| `agents:message` | `message:request`, `message:response`, `message:clear`, `message:cancel`, `message:error`, `tool:result`, `tool:approval`, `submission:create`, `submission:status`, `submission:error` | Chat message, tool, and Think submission lifecycle |
| `agents:chat` | `chat:request:failed`, `chat:recovery:*`, `chat:stream:stalled`, `chat:context:compacted` | Chat request, recovery, stream-stall, and context-compaction lifecycle |
| `agents:transcript` | `chat:transcript:repaired` | Transcript repair events |
| `agents:fiber` | `fiber:run:*`, `fiber:recovery:*` | Durable fiber lifecycle |
| `agents:agent_tool` | `agent_tool:recovery:*` | Parent/child agent-tool recovery |
| `agents:schedule` | `schedule:create`, `schedule:execute`, `schedule:cancel`, `schedule:retry`, `schedule:error`, `schedule:duplicate_warning`, `queue:create`, `queue:retry`, `queue:error` | Scheduled and queued task lifecycle |
| `agents:lifecycle` | `connect`, `disconnect`, `destroy` | Agent connection and teardown |
| `agents:workflow` | `workflow:start`, `workflow:event`, `workflow:approved`, `workflow:rejected`, `workflow:terminated`, `workflow:paused`, `workflow:resumed`, `workflow:restarted` | Workflow state transitions |
| `agents:mcp` | `mcp:client:preconnect`, `mcp:client:connect`, `mcp:client:authorize`, `mcp:client:discover` | MCP client operations |
| `agents:email` | `email:receive`, `email:reply`, `email:send` | Email processing |

## Subscribing to events

### Typed subscribe helper

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

```js
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();
```

```ts
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 fully typed — `event` is narrowed to only the event types that flow through that channel.

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

### Raw diagnostics\_channel

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

```js
import { subscribe } from "node:diagnostics_channel";

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

```ts
import { subscribe } from "node:diagnostics_channel";

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

## Tail Workers (production)

In production, all diagnostics channel messages are automatically forwarded to [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/). No subscription code is needed in the agent itself — attach a Tail Worker and access events via `event.diagnosticsChannelEvents`:

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

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

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

## Custom observability

You can override the default implementation by providing your own `Observability` interface:

```js
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;
}
```

```ts
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:

```js
import { Agent } from "agents";

class MyAgent extends Agent {
	observability = undefined;
}
```

```ts
import { Agent } from "agents";

class MyAgent extends Agent {
	override observability = undefined;
}
```

## Event reference

### RPC events

| Type | Payload | When |
| --- | --- | --- |
| `rpc` | `{ method, streaming? }` | A `@callable` method is invoked |
| `rpc:error` | `{ method, error }` | A `@callable` method throws |

### State events

| Type | Payload | When |
| --- | --- | --- |
| `state:update` | `{}` | `setState()` is called |

### 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 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](https://developers.cloudflare.com/agents/harnesses/think/recovery/#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 }` | 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

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

### 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 |

### 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](https://developers.cloudflare.com/agents/runtime/operations/observability/tracing/)

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

### [Configuration](https://developers.cloudflare.com/agents/runtime/operations/configuration/)

wrangler.jsonc setup and deployment.

### [Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/)

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

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/operations/observability/diagnostics-channels/#page","headline":"Diagnostics channels · Cloudflare Agents docs","description":"Subscribe to structured agent events for RPC calls, state changes, schedules, workflows, and MCP connections.","url":"https://developers.cloudflare.com/agents/runtime/operations/observability/diagnostics-channels/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-20","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```
