lua logs lists execution logs for the configured agent, newest first, filtered by log source, by a named primitive, by end user, by environment, or by a time window. It reads only.
Verified against lua-cli 3.39.0. --since, --until, --environment, and --follow ship with log drains in lua-cli 3.38.0; lua logs export and --channel are available from lua-cli 3.39.0. On an older CLI they are unknown options.
Synopsis
Description
Without--type, --name, --user-id, or --json, the command opens a menu: all agent logs, or a filter by skills, jobs, webhooks, preprocessors, postprocessors, devices, user messages, agent responses, agent errors, MCP tools, runtime, knowledge base (RAG), device triggers, or voice calls, with page navigation. With any of those flags it prints one page and exits. --ci does not stop the menu; pass --type or --json.
--type accepts all, calls, and each of the eighteen log sources: skill, job, webhook, trigger, preprocessor, postprocessor, mcp, device, device-trigger, user_message, agent_response, agent_error, runtime, rag, model-resolver, workflow, workflow-step, and workflow-script. Plural and short forms such as skills, pre, post, kb, error, response, and user are normalized. Any other value exits 2 and lists the valid values; that Valid values hint omits all and calls, which are handled before the check and are accepted. --name narrows one of the entity types (skill, job, webhook, preprocessor, postprocessor, device) to one primitive by name or id; it requires --type, is refused for the other types, and exits 3 when the primitive does not exist. --user-id keeps only logs produced for one end user. --agent-id reads another agent you administer instead of the one in lua.skill.yaml.
--since and --until bound the window, inclusive at both ends. Each takes an ISO 8601 instant (2026-09-21T09:00:00Z) or a relative form: a whole number and one of s, m, h, d, as in --since 15m or --since 7d. --environment production|sandbox keeps one environment; without it both are returned, since entries from the two are stored together. metadata.channel, where present, is dev for every message sent from lua chat in either environment, while end-user traffic carries its channel (pop for the web widget, whatsapp, slack, email, api, and so on). A tool that throws is written as an error entry under its own source (--type skill, or mcp); agent_error holds failures of the message pipeline itself, such as billing, validation, and model errors, so a post-release check reads both.
Pages hold --limit entries (default 20) and --page selects one (default 1); the server caps a page at 100 entries, so --limit 500 returns 100 and pagination.limit reports the cap. --page is deprecated in favour of the cursor the response carries, which pages a moving log without skipping or repeating entries. The API returns entries newest first and --json keeps that order; the text view prints a page oldest to newest so the latest entry is at the bottom, and ends with a hint block unless LUA_NO_HINTS is set.
The text view prints each entry as a block, never as a table row: a coloured level line, then the entry’s detail lines, then the message. Two of those detail lines are Env: and Version:, and each is printed only when the entry carries the value — an entry written before they were recorded renders exactly as it did before. They are detail lines, not columns, so nothing about the layout changes when they are absent.
--follow keeps the command open and prints new entries as they are written, polling about every two seconds from the last cursor and backing off when the server is unreachable; Ctrl-C ends it. It is refused together with --page. With --json it prints one complete { logs, nextCursor, pagination } envelope per poll that found new entries — the same shape and the same newest-first order as a single page, one JSON document at a time — so a reader can consume polls without re-assembling anything.
--type calls reads voice call records instead of execution logs, accepts --direction and --status, and prints a table or a different JSON shape. Call records are not execution logs and do not go through the same route, so --since, --until, --environment, and --follow are ignored with it rather than refused — --direction, --status, --limit, and --page are the only flags that reach the call records. A failure keeps the typed ✖ line on stderr even with --json; there is no JSON error envelope.
Arguments
None.Options
Output
--json prints { logs, nextCursor, pagination }. Each entry has id, timestamp, type (log or metric), subType (error, warn, info, debug, start, complete), message, an optional duration in milliseconds, and metadata with logSource, primitiveId, primitiveName, toolId, toolName, userId, agentId, runId, channel, environment, agentVersion, and executionId as available. There is no level field; filter errors on subType.
metadata.fields carries the structured fields an entry was written with by log.* — a flat object whose values are strings, numbers and booleans only. It is absent on an entry that had none, which is every entry written with console.*. An array field appears already joined, with its <key>.count beside it, and the keys are as you wrote them: the app. prefix a drain adds is not part of the entry. A call whose values were too long carries lua_fields_clamped: true beside them.
nextCursor is opaque; pass it back to read the next page, and treat null as the end. The boolean pagination.totalCountExact says whether totalCount is exact: above 10,000 matches it is false and totalCount is capped at 10000, so page with the cursor rather than trusting totalPages. hasNextPage is reliable either way. Trimmed to one entry:
Output
--type calls --json prints { calls, total, page, limit, totalPages }, where each call has startedAt, direction, from, to, status, and optional durationSeconds and recordingUrl:
Output
Examples
Check for errors after a test message or a promote:--type exits 2 and lists the accepted values:
Output
lua logs export
lua logs export starts an asynchronous, organization-wide export of the same records a log drain sends, written as gzipped NDJSON and handed back as signed links. Use it for a window that has already passed; a drain covers everything that happens next.
Exports are switched on per deployment. While they are off the route answers
404, so the command reports the export surface as not found. It may not be enabled on your deployment yet.--org, else the project in the current directory, else the credential’s own organization when it reaches exactly one.
Output
A relative
--since/--until is resolved by the server’s clock, exactly as it is for lua logs, so a skewed laptop cannot shift the window it asks for. Omit --until and the upper bound is the present moment — but the literal string now is not part of the vocabulary and is refused: pass an ISO 8601 instant or a relative form such as 15m, 2h or 7d.
Part files are named org-<orgId>-logs-<NNNN>.ndjson.gz, zero-padded so lexical order is emission order. A server-named part is always written under a sanitised basename inside --out and never outside it. On Lua’s side each part is stored under the log-exports/ key prefix — a signed link points at log-exports/<fileId>.gzip — which is what lets export parts expire on a storage lifecycle rule of their own.
What to know before you run it
- One export per organization at a time, and a second invocation attaches to the running one rather than refusing — so re-running the command in another terminal follows the same job to the same files.
- The window is at most 31 days and the job at most 10,000,000 records. Narrow the window or the sources rather than trying to raise either; a job that hits the row cap succeeds with a truncation warning rather than failing, and the command says so in capitals.
- The download links expire 24 hours after the export finishes — the links, not the files. Seven days after the export was started the job reads
expiredand loses its links for good, and the stored parts are removed on a clock of their own, between 7 and 8 days after each part was written. There is no supported way to re-issue a link for a finished job; start another export for the same window. Pass--outand keep the bytes rather than relying on any of the three clocks. - Message bodies are excluded. There is no
--include-contentflag: an export carrying end-user content is a deliberate act with a scope attached to it, so it goes through the API withlogs:manage, not through a flag on a download. - Your organization’s own scrub rules apply too. Lua’s built-in scrubbing runs on every record unconditionally, and on top of it an export applies the union of the rules you added yourself across your active drains — so a value a drain would mask as
[redacted:org:<your rule id>]carries the same mask here. Apauseddrain’s rules still count; adisabledor deleted one’s do not. It may not be enabled on your deployment yet; where it is not, only the built-in classes run, and the bytes carry whatever only your own rules would have caught.counts.orgScrubHitson the job tells the two apart. - Only the eighteen execution-log sources and
executionare exportable. The six adjacent-record sources —delivery,trigger-execution,job-execution,workflow-run,voice-call,workflow-audit— are built for a drain and exist nowhere else, so an export cannot produce them. - Each line is one complete batch, not one record. Log export has the shape and an
ajvsnippet that validates a part against the published schema.
GET /admin/orgs/:orgId/logs/download, the 10,000-row CSV, is deprecated in favour of this job. It still works and nothing about it has changed. The comparison table says what you gain.Exit codes
See also
- Read logs and debug — the verification loop after a promote
- Ship logs to your stack — copy these same records to a vendor, an OTLP collector, a bucket you own, or your own receiver
- Log export — the routes behind
lua logs export, its refusal codes, and the file format lua drains— configuring that copy- Troubleshooting
lua chat— what triggers the per-turn error probelua voice— where call records come from

