Skip to main content
Verified against the shipped 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 as lua 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
The mapping line is printed so you can see which drain answered. Under --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.
The challenge expires 10 minutes after 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 — a 4xx 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
The command returns as soon as the records are queued — it does not wait for the delivery, so unlike 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 the Authorization header 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 create and on rotate-secret.
A drain read back — by 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.
Twenty-five sources are selectable from 3.39.0, and the default set for a new drain gains four of them:
A drain you already created keeps the sources it was created with. The default set applies at creation and is never re-applied, so lua drains update is the only way to add a phase-2 source to an existing drain. The one exception is execution, which was already a default: a drain that selects it receives four new lua.execution.* outcome names with no opt-in.
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
Every table the command prints is fitted to 100 columns, and a value too long for its column is clipped with an … — 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
When the 30-second wait passes with nothing recorded, the same payload carries "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:
Archive into a bucket you own, then settle the ownership challenge:
Create a signed HTTPS drain, save the secret, and verify the endpoint:
Fail a CI step when any drain is failing. 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:
Look at why a drain is unhappy:
Replay every batch a receiver bug lost in the last hour, then watch for the new attempts:
A drain is limited to 10 replays an hour, so a loop over more than ten batches will meet DRAIN_REPLAY_RATE_LIMITED; read retryAfterS from the refusal rather than retrying straight away. Narrow a noisy drain to errors on one agent:
Pause during a receiver deploy, then resume. Records buffer while it is paused:
Rotate the signing secret with no delivery gap:

Errors

A 4xx 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 s3 flags, the bucket policy, and the ownership challenge confirm settles
  • 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 delivery source, its statuses and its masked recipient
  • Workflow audit records — the workflow-audit source, and the SIEM webhook migration
  • Delivery guarantees — retry, drops, heartbeat, and the health states behind status
  • Verify signatures — what rotate-secret changes 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 --type does not accept them, and lua logs takes no --org