wirldocs

Logs

What is captured#

Everything your server prints with console.log, console.info, console.warn, console.error and console.debug, and every uncaught exception, for every request: a page load, a webhook, a schedule run, a flow's step. Nothing to install and nothing to declare. The scaffold's server prints one line per request so a fresh app is never silent.

Lines are kept for seven days, grouped by the request that printed them, with what wirl knew about the request: who made it, whether a webhook was verified, which schedule run it was, the status the app answered, and the deployment version that was live.

A request made by someone signed in names them by first name, or the start of their email address, if you are a member of the app's organisation or made the request yourself. Anyone else who can read the logs, such as someone with an editor's share on the app and no seat in the organisation, reads someone else, as they read made by someone else of a connection's maker. Under --json, who says the same, and userId is that person's user id if you are a member of the app's organisation or made the request yourself, and null otherwise, as it is when nobody was signed in. If wirl no longer has an account for them, it reads someone who has left. The Logs page and a request's own page on the dashboard name them the same way.

Deployments made before logging existed have no lines; deploy again and they start.

A request wirl refuses before your app is called is not an invocation, so it is not in the logs: a public path's 429 over its ceiling, for one, which is counted as public.throttled instead, one line a minute at most, under Turned away at the edge on the app's Logs page, for anyone who can deploy it, and on the organisation's Activity page.

Logs are what your app printed, as far as Cloudflare kept them. Cloudflare's Workers Logs can miss lines, all of a request's among them: in one of wirl's own tests, a flow run of 3,001 steps left no line at all for 7 of them, though every step ran and the run recorded each one. So a count taken from logs can come up short, and the error counts the dashboard shows and wirl mails an app's owner are taken from them. A run's record is wirl runs show, whose --json has every attempt, and the run's page, not its logs: count a run's steps there, never from wirl logs.

Reading them#

wirl logs                 the last hour, newest first
wirl logs --errors        only requests that failed or printed at error level
wirl logs --since 24h     further back: 15m, 1h, 24h, 7d
wirl logs --follow        keep watching: every two seconds, everything since the last look
wirl logs --ray <id>      the one request behind a cf-ray header
wirl logs --run <id>      the one schedule run, or every step of a flow run
wirl logs --kind flow     one kind of request: page, webhook, schedule, flow or other
wirl logs --search text   requests whose path or lines contain the text, or whose status it is
wirl logs --cursor <n>    the page before a 'More before this' line

--follow starts with the newest five pages of the window --since opens, not all of it, then reads back to the last request it printed each time it looks, up to twenty pages. Whatever either bound leaves, it names on stderr, with the wirl logs --since ... --cursor ... command that reads it.

--search looks at three things the list shows of each request: its path, its status when the text is three digits such as 404, and the text of each of its lines, though not the fields your app logged beside that text (shown after it as JSON) nor a thrown error's stack. It never searches who made a request, nor the rest of what Cloudflare keeps about it, such as its query string and its headers, so it finds a person's email address only in a path or in a line your app printed. A search is at most 1,024 bytes. The Logs page's search box and the wirl_logs tool's search work the same way.

Each request is one block:

2026-09-17T05:57:04Z  POST /boom   Anyone   500 · exception · 0 ms   v3   ray a3c5df0f3a665163
  log    probe: handling POST /boom
  error  probe: boom on purpose
         at Object.fetch (worker.mjs:7:13)

Times are UTC. A flow's step names its run where a request names its ray: wirl sends a step from inside Cloudflare, not through the edge, so a step has no ray. After the run come the step's n, from the x-wirl-n header wirl sends with each step, and its name, failed or cancelled for wirl's own last calls to the app: run run_4vqabcdefghijklmnopqrstuvw n 2 step decide. A step wirl sent before it sent x-wirl-n shows no n, as run run_4vqabcdefghijklmnopqrstuvw step decide: Cloudflare's logs hide the value of the one other header that holds it. A step's page on the dashboard names it the same way.

A thrown error's stack names worker.mjs, the one file wirl bundles your server module and everything it imports from your files and packages into, with that file's line and column, not your server.mjs's: worker.mjs:7:13 above is line 7 of the bundle. wirl makes no source map, so it does not map a frame back to your own files; search them for the function the frame names, or for the message above it. wirl dev runs a bundle of its own, .wirl/worker.mjs, whose lines can differ from the deployed one's.

A coding agent reads the same thing through the wirl_logs MCP tool, and should, before changing code, whenever a deploy misbehaves or a schedule or webhook failed.

On the dashboard, the app's Logs page shows the same list with filters, and its Overview shows the last errors. Both are for people who can deploy the app: a line can contain whatever the app chose to print.

When the app does not answer#

When wirl gets no answer from your app that it can pass on, as when the app runs out of CPU time, it answers 502 itself. The cause it was given can be your app's own exception text, so that page names it only to someone whose pass or visit link says they may deploy the app. A pass says what its holder could do when it was made or last renewed, so someone moved from editor to viewer is still told until it renews or runs out, within 13 minutes, as when their access is removed (Limits). A preview's visit link is a sign-in as whoever made it, so anyone who opens it is told on that preview. The pass it gives says what its maker could do when the link was printed, not when the pass was made, so a link printed before a move from editor to viewer still tells its opener for up to 29 minutes after it was printed, as Limits says of a removal. Everyone else gets only The app is not responding. and, when it has one, the request's ray: an anonymous visitor, a viewer, every caller of a public path, whoever they are, and a schedule run, whose note is that answer. When it is wirl's own call into the app that failed, everyone gets The app did not answer., with the ray when it has one, and no cause. Look the ray up with wirl logs --ray <id>. A public app never sends a visitor to sign in, so on a public app you are told the cause only if you hold a pass for it already.

What the app called#

wirl egress lists the calls the app made out through wirl, newest first, whether wirl let them through or refused them:

wirl egress               the last hour
wirl egress --since 24h   further back: 15m, 1h, 24h, 7d
wirl egress --run <id>    one flow run's calls, or one schedule run's
wirl egress --cursor <n>  the page before a 'More before this' line

Each call is one line:

2026-10-03T04:30:22Z  POST api.stripe.com/v1/refunds/re_42   stripe   200 allowed   run run_4vqabcdefghijklmnopqrstuvw n 2 attempt 2

A line says when (UTC), the method and host, the connection, the status, and allowed or the code the call was refused with, such as not_allowed for a host the connection does not cover or no_key for a connection still waiting for its key. A flow step's call also names its run, the step's n, its attempt, and the path it called, so two calls of a retried step are told apart. The path is never the query string, where some vendors take a key, and no header or body of a call is recorded. A vendor that takes its key in the path itself, as a webhook URL does, has it recorded. A preview's calls are listed with the app's, and --run tells them apart. A flow step's call refused because its deadline had passed is not recorded at all.

Only the app's own calls, for the people who can deploy it, as its logs are, and at most seven days back. A coding agent reads the same through the wirl_egress MCP tool. The organisation's Activity page lists them too, for an admin.

What not to log#

A secret, a whole request body, or a person's data you would not want sitting in a seven-day-old line. A line per request is free; a line per loop iteration is not, and the plan's monthly allowance is shared by every app in the installation.