Summary
The observability MCP server (https://observability.mcp.cloudflare.com/mcp) cannot return any cron-triggered Worker event. Its response schema requires $workers.outcome to be a string, but the underlying Cloudflare telemetry API omits that field entirely for eventType: "cron" events. The tool therefore rejects valid data returned by the very API it wraps, and cron/scheduled Worker invocations become unqueryable through MCP.
Reproduce
Any Worker with a [triggers] crons schedule and observability enabled. Call query_worker_observability:
{
"view": "events",
"queryId": "repro",
"limit": 20,
"dry": false,
"parameters": {
"datasets": ["cloudflare-workers"],
"filters": [
{ "key": "$metadata.service", "operation": "eq", "type": "string", "value": "<worker-name>" },
{ "key": "$metadata.origin", "operation": "eq", "type": "string", "value": "cron" }
]
},
"timeframe": { "reference": "<now>", "offset": "-24h" }
}
Actual
Error analyzing worker logs: [
{
"code": "invalid_union",
"errors": [[{ "expected": "string", "code": "invalid_type",
"path": ["outcome"],
"message": "Invalid input: expected string, received undefined" }]],
"path": ["result", "events", "events", 0, "$workers"],
"message": "Invalid input"
},
...
]
Note the error names events[0], events[2], etc. — the events were retrieved; only deserialization fails.
Expected
The cron events are returned, as they are via the REST API.
Root cause
POST /accounts/{account_id}/workers/observability/telemetry/query returns different $workers shapes by event type. Same Worker, same account, same timeframe:
| eventType |
$workers keys |
outcome |
cron |
truncated, event, scriptName, eventType, scriptVersion, executionModel, requestId |
absent |
fetch |
event, truncated, scriptName, outcome, eventType, executionModel, scriptVersion, requestId, cpuTimeMs, wallTimeMs |
"ok" |
Cron events also omit cpuTimeMs and wallTimeMs.
The MCP layer's zod schema for $workers requires outcome, so every cron event fails validation. The REST endpoint returns these same events without complaint, which is the workaround (see below).
Suggested fix
Make outcome optional in the $workers schema (and likely cpuTimeMs / wallTimeMs too), or discriminate the schema on eventType.
Impact
Cron-triggered Workers cannot be observed through MCP at all. This matters for scheduled jobs, where the log line emitted by the scheduled() handler is often the only record that the job ran and what it decided — there is no request/response to inspect, and wrangler tail only shows live traffic, so a missed window is unrecoverable.
Secondary note
$metadata.origin for cron-triggered events is "cron", not "scheduled". Filtering on "scheduled" returns an empty array rather than an error, which is indistinguishable from "the cron never fired" — a misleading result when debugging a scheduled job. Worth documenting in the tool description alongside the existing $metadata.origin guidance.
Workaround (within MCP)
The failure is confined to the events view, which deserializes each event's
$workers object. The calculations view aggregates server-side and never
constructs that object, so it reads cron logs fine — including structured fields
from console.log('msg', {...}):
{
"view": "calculations",
"queryId": "cron-check",
"dry": false,
"parameters": {
"datasets": ["cloudflare-workers"],
"filters": [
{ "key": "$metadata.service", "operation": "eq", "type": "string", "value": "<worker>" },
{ "key": "$metadata.message", "operation": "eq", "type": "string", "value": "<log message>" }
],
"calculations": [{ "alias": "n", "operator": "count" }],
"groupBys": [{ "type": "boolean", "value": "<structured field>" }],
"havings": []
},
"timeframe": { "from": "…", "to": "…" }
}
Grouping by $metadata.message enumerates which cron log lines fired; grouping
by a structured field recovers that field's value. It is enough to confirm a
scheduled job ran and what it decided, without the raw event body.
This also gives a cheap probe for whether cron events exist at all: an events
query over a window that contains one crashes, whereas the same query over a
window without one returns []. The error is the signal.
Workaround (REST)
Query the REST API directly; it returns the events correctly:
POST /accounts/{account_id}/workers/observability/telemetry/query
{
"queryId": "…", "view": "events", "limit": 25, "dry": false,
"parameters": {
"datasets": ["cloudflare-workers"],
"filters": [
{ "id": "1", "key": "$metadata.service", "operation": "eq", "type": "string", "value": "<worker>" },
{ "id": "2", "key": "$metadata.origin", "operation": "eq", "type": "string", "value": "cron" }
],
"calculations": [], "groupBys": [], "havings": []
},
"timeframe": { "from": <epoch-ms>, "to": <epoch-ms> }
}
(The REST endpoint wants numeric epoch-ms from/to and id on each filter; the MCP tool accepts ISO strings and omits id.)
Summary
The observability MCP server (
https://observability.mcp.cloudflare.com/mcp) cannot return any cron-triggered Worker event. Its response schema requires$workers.outcometo be a string, but the underlying Cloudflare telemetry API omits that field entirely foreventType: "cron"events. The tool therefore rejects valid data returned by the very API it wraps, and cron/scheduled Worker invocations become unqueryable through MCP.Reproduce
Any Worker with a
[triggers] cronsschedule and observability enabled. Callquery_worker_observability:{ "view": "events", "queryId": "repro", "limit": 20, "dry": false, "parameters": { "datasets": ["cloudflare-workers"], "filters": [ { "key": "$metadata.service", "operation": "eq", "type": "string", "value": "<worker-name>" }, { "key": "$metadata.origin", "operation": "eq", "type": "string", "value": "cron" } ] }, "timeframe": { "reference": "<now>", "offset": "-24h" } }Actual
Note the error names
events[0],events[2], etc. — the events were retrieved; only deserialization fails.Expected
The cron events are returned, as they are via the REST API.
Root cause
POST /accounts/{account_id}/workers/observability/telemetry/queryreturns different$workersshapes by event type. Same Worker, same account, same timeframe:$workerskeysoutcomecrontruncated, event, scriptName, eventType, scriptVersion, executionModel, requestIdfetchevent, truncated, scriptName, outcome, eventType, executionModel, scriptVersion, requestId, cpuTimeMs, wallTimeMs"ok"Cron events also omit
cpuTimeMsandwallTimeMs.The MCP layer's zod schema for
$workersrequiresoutcome, so every cron event fails validation. The REST endpoint returns these same events without complaint, which is the workaround (see below).Suggested fix
Make
outcomeoptional in the$workersschema (and likelycpuTimeMs/wallTimeMstoo), or discriminate the schema oneventType.Impact
Cron-triggered Workers cannot be observed through MCP at all. This matters for scheduled jobs, where the log line emitted by the
scheduled()handler is often the only record that the job ran and what it decided — there is no request/response to inspect, andwrangler tailonly shows live traffic, so a missed window is unrecoverable.Secondary note
$metadata.originfor cron-triggered events is"cron", not"scheduled". Filtering on"scheduled"returns an empty array rather than an error, which is indistinguishable from "the cron never fired" — a misleading result when debugging a scheduled job. Worth documenting in the tool description alongside the existing$metadata.originguidance.Workaround (within MCP)
The failure is confined to the
eventsview, which deserializes each event's$workersobject. Thecalculationsview aggregates server-side and neverconstructs that object, so it reads cron logs fine — including structured fields
from
console.log('msg', {...}):{ "view": "calculations", "queryId": "cron-check", "dry": false, "parameters": { "datasets": ["cloudflare-workers"], "filters": [ { "key": "$metadata.service", "operation": "eq", "type": "string", "value": "<worker>" }, { "key": "$metadata.message", "operation": "eq", "type": "string", "value": "<log message>" } ], "calculations": [{ "alias": "n", "operator": "count" }], "groupBys": [{ "type": "boolean", "value": "<structured field>" }], "havings": [] }, "timeframe": { "from": "…", "to": "…" } }Grouping by
$metadata.messageenumerates which cron log lines fired; groupingby a structured field recovers that field's value. It is enough to confirm a
scheduled job ran and what it decided, without the raw event body.
This also gives a cheap probe for whether cron events exist at all: an
eventsquery over a window that contains one crashes, whereas the same query over a
window without one returns
[]. The error is the signal.Workaround (REST)
Query the REST API directly; it returns the events correctly:
(The REST endpoint wants numeric epoch-ms
from/toandidon each filter; the MCP tool accepts ISO strings and omitsid.)