Skip to main content
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.
The level sets the entry’s level and, on a drain, its severity: 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
ErrorsLuaLogFieldsError 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 the log.* call, before anything is written, when fields breaks one of the structural rules above. The message names the key and the rule.
Do not wrap every log.* call in a try. A LuaLogFieldsError means a key or a value type is wrong in your code, which is a fault to fix rather than one to survive — and it is deterministic, so it fails the first time the line runs. Catching it hides the mistake and keeps the field missing.

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.warn and console.error behave 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 line x {"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.log is still a debug entry, so a drain at --min-severity info still does not receive it. If you want a line at info, write log.info or console.info.

See also