Debugging Deployed OS Workers
Assume we are debugging:
- OS at
https://os.iterate.com - project ingress under
iterate.app - project slug
iterate
Use preview_N or prd configs for deployed workers. For local dev, use
dev or dev_<user> with pnpm dev; scripts discover the running server from
.dev-server/dev-server.json. Run CLI commands from apps/os; plain
pnpm cli ... uses local Doppler setup, and
doppler run --config <config> -- pnpm cli ... targets a specific deployment.
Common CLI Targets#
# Local dev through the discovery file. Requires `pnpm dev` or `pnpm dev start --detach`.
pnpm cli itx --help
# Fully-local dev server with an explicit config.
doppler run --config dev -- pnpm cli itx --help
# Production.
doppler run --config prd -- pnpm cli itx --help
# Active preview slot. Use a healthy leased preview, not a stale slot number
# (`doppler run --project _shared --config prd -- pnpm preview status` shows
# each slot's holder and expiry).
doppler run --config preview_3 -- pnpm cli itx --helpProject Interaction Paths#
itx CLI#
Prefer this first. It is typed through the live project capability handle and avoids hand-assembling URLs.
doppler run --config prd -- pnpm cli itx run \
--context <prj_id> \
-e 'return await itx.streams.get("/debugging-docs/example").getEvents({ limit: 100 })'Append and read in one script:
doppler run --config prd -- pnpm cli itx run \
--context <prj_id> \
-e 'const stream = itx.streams.get("/debugging-docs/example"); const [appended] = await stream.append({ type: "events.iterate.com/debugging-docs/example", payload: { source: "itx" } }); const history = await stream.getEvents({ afterOffset: appended.offset - 1 }); return { appended, history }'List project IDs with the CLI's platform-wide admin-secret authority:
doppler run --config prd -- pnpm cli itx run \
--eval 'return await itx.projects.list()'--context takes the project ID (prj_…).
Project MCP#
The OS MCP transport is the /api/mcp Start route. Production
advertises https://mcp.iterate.com as the canonical OAuth resource URL, and
ingress rewrites that hostname to the same route. The app-host
https://os.iterate.com/api/mcp route is also valid. Fully-local dev defaults
to <baseUrl>/api/mcp. /projects/:slug/mcp is the dashboard UI, not the
transport URL. Admin-token sessions expose all projects and the exec_typescript tool
requires a project slug when it runs.
https://mcp.iterate.comStart Claude with that MCP server preconfigured:
cd apps/os
doppler run --config prd -- pnpm cli claude-mcpBy default the MCP server exposes only exec_typescript. If you deliberately want the
plain-language project-agent bridge too, opt in with the URL parameter via the
CLI flag:
doppler run --config prd -- pnpm cli claude-mcp --with-agentFor previews, run under the preview Doppler config:
doppler run --config preview_3 -- pnpm cli itx run --eval 'return await itx.projects.list()'
doppler run --config preview_3 -- pnpm cli claude-mcpOr leave it running in tmux:
tmux new -s os-iterate-mcp 'cd apps/os && doppler run --config prd -- pnpm cli claude-mcp'Authentication#
Most operator paths use the OS admin bearer token from the deployment's Doppler
config. The pnpm cli itx ... commands read APP_CONFIG_BASE_URL and
APP_CONFIG_ADMIN_API_SECRET from Doppler.
Browser debugging can also authenticate through the Iterate Auth Worker.
For a preview, change the config and discover a current project first. Avoid hard-coding disposable preview projects.
doppler run --config preview_3 -- pnpm cli itx run \
--eval 'return await itx.projects.list()'Useful itx Snippets#
# Confirm the project resolves and list its capabilities.
doppler run --config prd -- pnpm cli itx run \
--eval 'return await itx.projects.get("<prj_id>").__describe()'
# List the project's streams.
doppler run --config prd -- pnpm cli itx run \
--context <prj_id> \
--eval 'return await itx.streams.list()'
# Read a stream.
doppler run --config prd -- pnpm cli itx run \
--context <prj_id> \
--eval 'return await itx.streams.get("/debugging-docs/example").getEvents({ limit: 100 })'
# Inspect an agent's processor runtime state.
doppler run --config prd -- pnpm cli itx run \
--context <prj_id> \
--eval 'return await itx.agents.get("/agents/default").processor.getRuntimeState()'Cloudflare Debugging#
Cloudflare MCP Server#
Use the Cloudflare API MCP server for Workers traces, routes, bindings, Durable Objects, and other Cloudflare state. Docs: Cloudflare MCP servers.
OS is ONE worker per environment; the name is the env's osWorkerName in
the root envs.ts (os-prd, os-preview-N). Dashboard, api, and every
Durable Object class all live in that one script — there is no per-DO
worker to pick. See worker-topology.md.
Persistent Workers Logs and traces are declared in
apps/os/scripts/generate-wrangler-config.ts (the observability block).
Ray IDs are especially useful. If you have a Ray ID, first find the matching
log event, then use its traceId to fetch the trace and all span events.
With the general Cloudflare API MCP server, use search to find endpoint
shapes, then execute with a Workers Observability telemetry query.
For production OS request traces, use the otel dataset. The workers
dataset may expose metadata keys but can return no request rows for the app
traffic you are trying to inspect. The production service name is os-prd;
do not filter on $metadata.service == "os".
Start with recent events, then use the returned traceId to inspect the
whole trace. Keep the response compact by mapping the Cloudflare result before
returning it:
async () => {
const from = Date.parse("2026-07-02T12:15:00.000Z");
const to = Date.parse("2026-07-02T12:45:00.000Z");
const resp = await cloudflare.request({
method: "POST",
path: `/accounts/${accountId}/workers/observability/telemetry/query`,
body: {
queryId: "prod-os-mcp-events",
timeframe: { from, to },
view: "events",
limit: 200,
parameters: {
datasets: ["otel"],
filters: [
{
key: "$metadata.service",
operation: "in",
type: "string",
value: "os-prd",
},
],
needle: { value: "mcp", matchCase: false },
},
},
});
return (resp.result?.events?.events ?? []).map((event) => ({
iso: new Date(event.timestamp).toISOString(),
service: event.$metadata?.service,
level: event.$metadata?.level,
error: event.$metadata?.error,
requestId: event.$metadata?.requestId,
traceId: event.$metadata?.traceId,
tx: event.$metadata?.transactionName,
url: event.source?.url?.full,
method: event.source?.http?.request?.method,
status: event.source?.http?.response?.status_code,
ua: event.source?.user_agent?.original,
ray: event.source?.cloudflare?.ray_id,
}));
};To discover current keys or values, use keys and values. The values
endpoint requires timeframe and type:
async () => {
const timeframe = {
from: Date.parse("2026-07-02T12:15:00.000Z"),
to: Date.parse("2026-07-02T12:45:00.000Z"),
};
return cloudflare.request({
method: "POST",
path: `/accounts/${accountId}/workers/observability/telemetry/values`,
body: {
datasets: ["otel"],
timeframe,
key: "$metadata.service",
type: "string",
limit: 100,
},
});
};For status-code breakdowns, the current key is
http.response.status_code, not $metadata.statusCode.