lua logs and still counts against the platform’s own retention window.
Drains exist so the logs you already have stop being a place you visit and start being a place that pages you. The monitor packs turn a working drain into a firing alert in a few minutes.
Log drains are switched on for a deployment as a whole, not per organization. While they are off,
lua drains and the drain routes answer 404 — the resource does not exist rather than being forbidden, so a caller cannot tell the feature apart from a typo. If every verb answers 404, ask [email protected].What a drain is
A drain is a rule that lives on the organization, not on one agent. One drain can cover every agent in the organization, including agents created after the drain was, or a named list. That is deliberate: a platform team configures one destination and one credential, and application teams keep shipping agents into it without touching it. Each drain has:
Create one with
lua drains create, over the REST API, or from the admin dashboard.
What arrives
Every delivery is a batch: oneresource object describing the agent, and up to 500 records describing what happened. The shape is versioned and published as a JSON Schema at https://docs.heylua.ai/schemas/logs/1.0; the field-by-field reference is on Event schema.
records[].id, never on X-Lua-Batch-Id.
Selectors
Selectors decide what a drain copies. They are evaluated per record, before anything leaves the platform.Sources
A source is where a record came from. Nineteen are selectable in this phase: the eighteen agent log sources plusexecution, which carries the start, completion, and failure of an execution rather than a line your code printed.
user_message and agent_response carry end-user content. They are off unless you turn on --include-content, which prints an acknowledgement you have to accept before the drain is created. Even then, every record still passes the scrubber, and a drain’s redact list drops named attributes — user.id, for instance — before the record is encoded.
Severity
--min-severity keeps records at or above one of debug, info, warn, error. The default is info, which drops debug lines — the bulk of most agents’ volume.
--sampling debug=0.1 keeps a deterministic tenth of debug records instead of all or none. Sampling is by execution, so a sampled execution keeps all of its debug lines rather than a scattered fraction of them; only debug is sampleable.
Agents and environments
--agents all (the default) covers every agent in the organization, now and later. --agents <id>,<id> pins the drain to a list. --environments production (the default) or --environments production,sandbox decides whether the sandbox is copied too; a sandbox drain is the fastest way to prove a destination works before production traffic reaches it.
States
A drain moves through six states. Delivery only happens in three of them.
A drain that stays in
failing for 24 hours pauses itself. Resuming always lands in degraded, never straight in healthy: it has to prove itself again. Changing the endpoint or the type sends a drain back to pending_verification, and records buffer while you re-verify. Delivery guarantees has the full transition table.
Output
lua drains status exits 2 when any drain is failing. That is also the usage exit code, so a guard that must tell a failing drain from a mistyped command reads the state field in --json rather than the exit status.
What never leaves Lua
Three things hold, on every drain, with no way to turn them off:- Credential shapes are masked. Every record body and every string attribute passes a scrubber before it is encoded. It replaces recognizable credentials — bearer and basic headers, JWTs, private-key blocks, connection-string passwords, URL secret parameters, Lua API keys and handoff codes, and well-known vendor keys from Stripe, Slack, GitHub, Twilio, Google, OpenAI, and Anthropic — with a marker such as
[redacted:vendor-key]. Your organization can add up to 20 regular expressions of its own, each at most 256 characters, masked as[redacted:org:<rule-id>]. - Destination credentials are never readable. Header values and the HMAC signing secret are write-only. A read of a drain returns header names and the last four characters, never a value; the signing secret is shown exactly once, when it is created or rotated.
- No request body is stored. The delivery log keeps the status code, latency, byte count, error class, and up to 1 KB of the destination’s response — scrubbed. It has no field that can hold what was sent.
console.log is stored as printed, and the scrubber only catches shapes it recognizes.
Quotas and limits
Going over the daily quota degrades the drain in steps rather than cutting it off:
warn and error records are the last to go: the ladder never drops them before it pauses the drain. Quota counters reset at 00:00 UTC and are visible on lua drains status --json under quota.
Egress allow-listing
Deliveries leave from Lua’s own network, not from your agent’s sandbox, so an IP allow-list needs the platform’s egress address rather than a DNS name. There is one, ineu-west-1:
production and sandbox agent environments — a drain with --environments production,sandbox needs nothing else allow-listed. The same value is served machine-readably from https://docs.heylua.ai/drains/egress.json, so an allow-list can be refreshed from CI rather than copied by hand. An address changes rarely and never without notice on this page.
Every delivery identifies itself with User-Agent: LuaDrain/1.0 (+https://docs.heylua.ai/drains).
The X-Lua-* protocol headers ride the generic HTTPS drain only, which is also the only destination whose batches are signed: X-Lua-Batch-Id, X-Lua-Drain-Id, X-Lua-Schema, and X-Lua-Signature (see Verify signatures). An OTLP delivery carries X-Lua-Batch-Id and nothing else of ours. The Datadog and Better Stack presets carry none of them: a vendor intake gets the vendor’s own authentication header, and the batch and drain ids travel inside the record instead. Route a shared receiver by X-Lua-Drain-Id only when it is a generic HTTPS drain that reaches it.
Pick a destination
Generic HTTPS
Your own receiver. Signed, with a copy-paste Node example.
OpenTelemetry
OTLP/HTTP into a collector, Loki, or anything downstream of one.
Datadog
Log Explorer, with
service, status, and env already right.Better Stack
A hosted source, with levels mapped.
Next steps
Event schema
Every field, the severity table, and the versioning policy.
Delivery guarantees
At-least-once, batching, retry, and what a drop looks like.
lua drains
Every verb, flag, and
--json shape.Log drains REST
The routes, bodies, and error codes.

