lua drains source. 3.38.0 was the first release carrying the command; the phase-2 source names below, replay, confirm and addressing a drain by name are available from lua-cli 3.39.0.
lua drains manages the organization’s log drains: the rules that copy agent execution logs to a destination you own. Every action runs against the organization, not against one agent, and takes effect for the next flush.
Every verb needs a credential holding logs:manage on the organization, which organization admins and owners have. It is a sensitive scope: a wildcard grant does not satisfy it, so a scoped key must name it.
Log drains are switched on for a deployment as a whole, not per organization. While they are off, every verb answers
404 — the resource does not exist rather than being forbidden.Individual destination types are opened separately. --type accepts ten values, but a deployment may not offer all of them yet: a create naming one that is closed answers 422 DRAIN_TYPE_UNAVAILABLE with the live list in its message. confirm and the --bucket family of flags only do anything once s3 is open.Synopsis
Description
With no action the command lists the organization’s drains, exactly aslua drains list does — drains are an organization resource, so there is nothing to pick first. With an action it runs once and exits. Every verb supports --json.
--org <id> chooses the organization and is accepted by every verb. Without it the command uses the organization of the project in the current directory; outside a project directory it uses the credential’s organization when the credential reaches exactly one. A credential that reaches several organizations must pass --org, and the refusal lists the ids you can choose from.
list prints every drain in the organization. status adds health and quota; with an id or a name it prints one drain in detail. It exits 2 when any drain is in state failing, and sets that code after printing, so --json still emits its full payload. deliveries prints recent delivery attempts for one drain, newest first.
Addressing a drain
status and deliveries take an id or an exact drain name. Every other verb — update, delete, test, verify, confirm, pause, resume, replay, rotate-secret — stays id-only, because an id is an unambiguous handle and a rename between reading a name and acting on it must never silently retarget a call that changes something.
Output
--json it is suppressed: stdout is the payload and nothing else.
Matching is exact and case-sensitive, and it happens on your machine before the server is asked. Four things can happen, and each has its own exit code:
The two refusals are deliberately exit
2 and not the 3 an unknown id gets. “You typed a name I do not have” is a statement about your argument; “that drain does not exist” is a statement the server made, and a script that branches on the exit code should be able to tell them apart.
Names are unique among an organization’s live drains, so the ambiguous branch should be unreachable — it exists anyway. A refusal quotes ids and names only: a drain also carries secretLast4 and a last4 per header, and those are credential fragments that do not belong in an error message.
lua drains status with an empty argument is still “no argument”: it lists every drain, rather than refusing a drain named "".
create and update take the same flags; update leaves out --type, which cannot be changed after creation — delete the drain and create a new one instead. Changing --endpoint sends the drain back to pending_verification and stops delivery until you re-verify; records keep buffering while you do.
verify re-runs ownership verification. test sends one lua.drain.test record through the real delivery path and prints the status code, latency, and response excerpt. pause stops delivery but keeps buffering; resume returns the drain to degraded, never straight to healthy. delete is a soft delete: the destination’s credentials are purged and queued records are dropped.
confirm settles an object-storage drain’s ownership challenge and applies to --type s3 alone. verify writes _lua-verify-<token> under the drain’s prefix; if the credentials can read it back, verification finishes on its own. With the write-only grant we recommend they cannot, so you list your own prefix, open the object, and echo the token. Omit --token outside --ci and the CLI prompts for it.
verify mints it; run verify again for a new one. Lua stores only a hash of the token and returns it from no route, which is why finding it in the bucket is the proof.
Both test and verify post the request and then wait for its outcome, because the delivery itself happens on the platform: test waits up to 30 seconds for the delivery attempt to land, verify up to 60 seconds for the verification result. The wait is not the operation — the request is already enqueued when the wait starts — so when nothing has landed in time the command exits 1 and names the command that will show the outcome once it does: lua drains deliveries <id> --kind test after a test, lua drains status <id> after a verification. With --json the payload is still printed, with pending: true on it.
rotate-secret mints a new HMAC signing secret and prints it once. It opens a 24-hour window in which deliveries carry two signatures, so a receiver can be updated without dropping a batch; --finalize closes the window early. See Verify signatures.
Replaying a lost batch
A terminal refusal — a4xx that is not 408 or 429 — drops the whole batch and never re-sends it. replay puts its records back in the queue. It is the one verb that takes two positionals: the drain id, then the batch id off a delivery row. Like every verb that changes something, it takes an id and not a name.
Output
test and verify there is no timeout and no pending: true. The attempt that carries them appears on deliveries with replayOf set to the batch id you passed.
The whole batch is re-sent, including records the destination already accepted, which is safe because consumers deduplicate on records[].id and a replay does not change one. Only --kind batch rows can be replayed: a test, a verification, a heartbeat, a drop notice, and a truncation notice are the drain talking about itself, and each has its own verb.
The window is six hours, measured on each record’s own timestamp rather than on when the batch failed — the same delivery horizon that bounds the buffer. A batch with nothing left inside it answers DRAIN_REPLAY_EXPIRED, and a drain is limited to 10 replays an hour.
Secrets are never flags
Header values and signing secrets never appear on a command line, where a shell history or a CI log would keep them.--header <name>names a header and the CLI prompts for its value, hidden as you type.--header-from-env <name>=<VAR>reads the value from the named environment variable instead. This is the form for--ci, where prompting is not possible.--credentials-from-env <KEY_ID_VAR>=<SECRET_VAR>names the two environment variables holding an object-storage key pair. Neither value reaches the command line. The access key id is stored and shown, because SigV4 puts it in theAuthorizationheader of every request in clear; the secret goes to the encrypted credential store and appears in no response.- The HMAC signing secret is not an input at all. The platform mints it and prints it once, on
createand onrotate-secret.
list, status, or the REST API — carries header names and the last four characters of each value, never a value. The same holds for an object-storage secret (bucket.credentials.secretLast4) and for a credential embedded in an endpoint (endpointSecretLast4).
--token on confirm is the one apparent exception, and it is not one: the token is a one-time ownership challenge that you read out of your own bucket, not a credential, and Lua never held the value to begin with.
Sources
--sources takes a comma-separated list and replaces the drain’s list rather than adding to it, so an update names every source you want to keep. Leave the flag off on create and the drain gets the default set; outside --ci the interactive picker offers every source with the defaults pre-ticked.
Six source names are available from lua-cli 3.39.0:
delivery, trigger-execution, job-execution, workflow-run, voice-call, and workflow-audit. An earlier CLI refuses them before it sends anything — --sources "<name>" is not a selectable source., exit 2 — so upgrade the CLI to select them.voice-call and workflow-audit are opt-in for reasons that have nothing to do with content: voice-call is narrower and more sensitive than a job outcome, and workflow-audit is a compliance stream an operator chooses deliberately. Naming either prints no acknowledgement and asks for no confirmation.
Content acknowledgement
--include-content turns on the user_message and agent_response sources, which carry what end users said and what the agent replied. The flag prints an acknowledgement and then requires a confirmation; --yes gives that confirmation non-interactively. Without either, the command prints the acknowledgement and exits non-zero without creating anything.
The confirmation is kept, not just consumed. The drain records who gave it and when, as contentAck on lua drains list --json, so “who agreed to send message content to this destination” is a question the drain itself answers. It is asked for on the transition — creating a content-carrying drain, or an update that turns content on — so an update that leaves content on as it was is not asked again, and one that turns it off is not asked at all.
lua drains update <id> --no-include-content turns content off again. It leaves --sources exactly as they are: user_message and agent_response stay selected and their records keep arriving, with the body withheld and lua.content.withheld set — so you still see that a turn happened, on what channel and for which end user, without the words. Drop the sources too if you want the records gone. Turning content off and naming a content source in the same update is refused: send the sources first, then --no-include-content.
Only user_message and agent_response are content sources. A drain that selects phase-2 sources and no content source never prints the acknowledgement and is never refused by --no-include-content. The flag still governs a handful of their attributes — the full delivery recipient, a trigger payload and response, a workflow event’s data, a recording URL, and the audit evidence group — which are withheld without it while the record itself still arrives.
Arguments
Options
Output
list prints a box-drawn table:
Output
… — a drain name here, a pause reason or a response excerpt elsewhere. --json is never abridged, so read it rather than the table whenever a full value matters.
--json returns the drains exactly as the API models them, with header values and the signing secret absent:
lua drains list --json
status <id> prints one drain as a labelled block rather than a row:
Output
ownership is what the last verification established, which verifiedAt alone does not say: verified when the destination could not have been anyone else’s, reachability-only when a host you chose simply answered 2xx. A drain verified before the field existed prints reachability-only, the weaker of the two readings. Protecting your destination has which destination earns which, and why verified on a vendor preset does not mean the account is yours.
status --json returns the same drains trimmed to what an operator watches — id, name, state, health, and quota, and no verification. Read lua drains list --json, which returns the full drain, when you need ownership or contentAck in a script:
lua drains status --json
create --json is the only place the signing secret appears. It is present for http drains, which are signed, and absent for the others:
lua drains create --json
test --json reports the round trip:
lua drains test --json
"ok": false, "pending": true, and null for the fields the attempt would have filled.
replay --json reports what was queued, and nothing about the delivery, which has not happened yet:
lua drains replay --json
deliveries --json returns { deliveries, nextCursor }, one entry per attempt, with the status code, latency, byte counts, error class, and a scrubbed response excerpt — never the request body.
Examples
Create a Datadog drain in CI, reading the API key from the environment:status sets exit code 2 for this after it has printed, so --json still emits its payload — but 2 is also the usage exit code, so a guard that has to tell the two apart reads state from that payload:
DRAIN_REPLAY_RATE_LIMITED; read retryAfterS from the refusal rather than retrying straight away.
Narrow a noisy drain to errors on one agent:
Errors
A4xx with a code prints <code>: <message>, plus the offending field when the server names one.
verify and test are budgeted per drain, over a rolling hour — 5 and 10 — and both refusals carry retryAfterS, the real number of seconds until the oldest attempt falls out of the window. That is usually minutes rather than the second or so DRAIN_RATE_LIMITED wants, which is why the three codes are worth telling apart rather than retrying on 429 alone.
A 403 prints:
Exit codes
See also
- Ship logs to your stack — what a drain is, what it selects, and every destination type
- Object storage — the
s3flags, the bucket policy, and the ownership challengeconfirmsettles - Protecting your destination — where a drain may point, and what each verification mode proves
lua logs export— a one-off NDJSON export of a window that has already passed- Delivery records — the
deliverysource, its statuses and its masked recipient - Workflow audit records — the
workflow-auditsource, and the SIEM webhook migration - Delivery guarantees — retry, drops, heartbeat, and the health states behind
status - Verify signatures — what
rotate-secretchanges on the wire - Log drains REST — the same operations over HTTP
lua logs— reading the eighteen execution-log sources from the platform’s own copy. The adjacent-record sources (delivery,trigger-execution,job-execution,workflow-run,voice-call,workflow-audit) are built for the drain and exist nowhere else:lua logs --typedoes not accept them, andlua logstakes no--org

