log.debug, log.info, log.warn and log.error write one log entry each: a message, and an optional set of fields that travel beside it rather than inside it. The entry appears in lua logs like any other, with its fields under metadata.fields in --json, and a log drain carries them to your destination as searchable app.* attributes — see Emit your own fields.
Available wherever agent code runs: tools and conditions, skills, webhooks, triggers, jobs, preprocessors, postprocessors, MCP servers and device handlers. Like env, the platform injects it into every one of those contexts.
log is not in lua-cli 3.38.0; it arrives in a later release, whose number is not settled as this is written.
Quick example
Describe what happened in the message, and put the values you will want to group by in the fields.Functions
log.debug(message, fields?), log.info, log.warn, log.error
Writes one log entry. The four methods differ only in the entry’s level.string
required
The line as a person reads it. It becomes the entry’s message, and a drain record’s
body.LogFields
Up to 32 keys, counting the
.count companion a string[] field adds. Omit it and the call is equivalent to the matching console method with a single string argument — no fields on the entry, and no lua.log.structured marker on a drain record.
It does not change where the entry comes from. A call in a skill is a
skill entry, with the eventName lua.skill.<level>, so lua logs --type skill and a drain selecting --sources skill both pick it up unchanged.
Returns — nothing.
Example
LuaLogFieldsError when fields is structurally invalid. A value that is merely too long never throws.
Types
LogFields
string
Matches
^[a-z][a-z0-9_]{0,63}$ — starts with a lower-case letter, then lower-case letters, digits and _, at most 64 characters. A key that looks like a credential is refused on top of that, because a key is never scanned by the scrubber the way a value is: the five credential shapes spellable in lower-case and underscores — a legacy Lua API key, a GitHub token, a Stripe key, a Lua handoff code and a Lua scoped key — are rejected at the call. lua_fields_clamped is reserved for the platform and refused as well.string | number | boolean | string[]
A
string[] is a convenience at the call site, not a wire type: it is sent as its elements joined with ,, plus a companion <key>.count that counts as one of your 32 keys. An element may not itself contain a comma. null, undefined, a nested object, an array of anything but strings, a Date and a BigInt are all refused — convert them yourself, or leave the key out. NaN and Infinity are refused too: neither has a JSON form.The limits
Structure throws; length does not. A structural mistake is the same on every call the line makes, so you meet it the first time you run the tool, under
lua test, rather than on the one production call whose payload was unusual. A length, by contrast, is data — the single customer with a two-kilobyte ticket title must not be the one who breaks a working tool — so an over-long value is cut to fit and the entry records that it was.
How a clamp works. Any value over 1 KiB is cut to 1 KiB. If the whole map — measured as the JSON form of the fields, marker included — is still over 8 KiB, every string value is cut to one common byte length, the largest that lets the map fit. No key is ever dropped and a cut never splits a character. The call then carries the reserved field lua_fields_clamped: true, which a drain delivers as app.lua_fields_clamped. Numbers and booleans are never clamped.
LuaLogFieldsError
Thrown synchronously by thelog.* call, before anything is written, when fields breaks one of the structural rules above. The message names the key and the rule.
Where log is, and is not
console is unchanged
log.* is a second way to write a line, not a replacement for the first. Nothing about console moves:
console.log,console.info,console.warnandconsole.errorbehave exactly as before, and other console methods are still not provided.- An object passed to one of them is still serialized into the message text —
console.info('x', { a: 1 })is still the linex {"a":1}. That is why fields are a separate call: an overload could not tell “log this object as text”, which existing code relies on, from “attach these fields”. console.logis still adebugentry, so a drain at--min-severity infostill does not receive it. If you want a line atinfo, writelog.infoorconsole.info.
See also
- Emit your own fields — what the fields become at each destination, and how redaction and scrubbing reach them
lua logs— reading entries, andmetadata.fieldsunder--json- Event schema — the record a drain delivers, and the
app.*attributes fields become - Logs and debugging — finding the entry you want
- Protecting your destination — the scrubber, and what it does not reach

