> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# lua drains

> Create, inspect, verify, test, pause, and rotate the organization's log drains, as text or JSON

*Verified against lua-cli 3.38.0, the first release carrying `lua drains`.*

`lua drains` manages the organization's [log drains](/drains/overview): 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.

<Info>
  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.
</Info>

## Synopsis

```bash theme={null}
lua drains list [--json]
lua drains status [<id>] [--json]
lua drains deliveries <id> [--limit <n>] [--kind <kind>] [--json]
lua drains create [--name <name>] [--type <type>] [--endpoint <url>] [--site <site>]
                  [--format json|ndjson] [--header <name>]... [--header-from-env <name>=<VAR>]...
                  [--sources <a,b>] [--min-severity <level>] [--agents all|<id,id>]
                  [--environments <production,sandbox>] [--sampling debug=<n>]
                  [--include-content] [--yes] [--json]
lua drains update <id> [same flags] [--no-include-content] [--json]
lua drains delete <id> [--yes] [--json]
lua drains test <id> [--json]
lua drains verify <id> [--json]
lua drains pause <id> [--reason <text>] [--json]
lua drains resume <id> [--json]
lua drains rotate-secret <id> [--finalize] [--json]

# every verb also takes --org <id>
```

## 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 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.

`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](/drains/generic-https#how-ownership-verification-works). `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.

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](/drains/verify-signatures#rotate-a-secret).

### 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.
* 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.

### 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.

`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`.

## Arguments

| Argument | Values                                                                                                             | Description                                                   |
| -------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| `action` | `list`, `status`, `deliveries`, `create`, `update`, `delete`, `test`, `verify`, `pause`, `resume`, `rotate-secret` | Omit it to list the drains.                                   |
| `<id>`   | A drain id, `drn_…`                                                                                                | Required by every verb except `list`, `status`, and `create`. |

## Options

| Option                           | Description                                                                                                                        | Default                                                           |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `--name <name>`                  | 1–64 characters, unique in the organization.                                                                                       | prompted                                                          |
| `--type <type>`                  | `http`, `otlp`, `datadog`, or `betterstack`. `create` only.                                                                        | prompted                                                          |
| `--endpoint <url>`               | The destination URL. Required for `http`, `otlp`, and `betterstack`; refused for `datadog`, whose URL is derived from `--site`.    | —                                                                 |
| `--site <site>`                  | `datadog` only: `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com`, `us5.datadoghq.com`, `ap1.datadoghq.com`, or `ddog-gov.com`. | —                                                                 |
| `--format <format>`              | `json` or `ndjson`. `http` only; OTLP and the vendor presets fix their own encoding.                                               | `json`                                                            |
| `--header <name>`                | Add a header; the value is prompted. Repeatable, up to 10.                                                                         | none                                                              |
| `--header-from-env <name>=<VAR>` | Add a header, reading its value from an environment variable. Repeatable.                                                          | none                                                              |
| `--sources <a,b>`                | Comma-separated [sources](/drains/overview#sources).                                                                               | the default set                                                   |
| `--min-severity <level>`         | `debug`, `info`, `warn`, or `error`.                                                                                               | `info`                                                            |
| `--agents all\|<id,id>`          | `all` for every agent in the organization, or a comma-separated list.                                                              | `all`, or the project's agent inside a project directory          |
| `--environments <list>`          | `production`, `sandbox`, or both, comma-separated.                                                                                 | `production`                                                      |
| `--sampling debug=<n>`           | Keep a deterministic fraction of `debug` records, `0 < n <= 1`. Only `debug` is sampleable.                                        | all                                                               |
| `--include-content`              | Turn on `user_message` and `agent_response`. Needs a confirmation or `--yes`.                                                      | off                                                               |
| `--no-include-content`           | `update` only: stop sending message bodies. The drain keeps its sources.                                                           | —                                                                 |
| `--limit <n>`                    | `deliveries` only: attempts to print, at most 100.                                                                                 | `20`                                                              |
| `--kind <kind>`                  | `deliveries` only: `batch`, `test`, `verify`, `heartbeat`, `dropped`, or `truncated`.                                              | all                                                               |
| `--reason <text>`                | `pause` only: recorded with the pause.                                                                                             | none                                                              |
| `--finalize`                     | `rotate-secret` only: close an open rotation window.                                                                               | off                                                               |
| `--yes`                          | Skip the confirmation on `delete` and on `--include-content`.                                                                      | off                                                               |
| `--org <id>`                     | The organization to act on. Accepted by every verb.                                                                                | the project's organization, or the credential's only organization |
| `--json`                         | Print JSON.                                                                                                                        | off                                                               |

## Output

`list` prints a box-drawn table:

```bash theme={null}
lua drains list
```

```text Output theme={null}
┌──────────────────┬─────────────┬──────────┬────────────┬────────┬─────────┬──────────────────────┐
│ NAME             │ TYPE        │ STATE    │ ENV        │ AGENTS │ BACKLOG │ LAST SUCCESS         │
├──────────────────┼─────────────┼──────────┼────────────┼────────┼─────────┼──────────────────────┤
│ Datadog prod     │ datadog     │ healthy  │ production │ all    │ 0       │ 2026-09-21T09:14:02Z │
│ Receiver (dev)   │ http        │ degraded │ sandbox    │ 2      │ 142     │ 2026-09-21T09:08:47Z │
│ OpenTelemetry c… │ otlp        │ paused   │ production │ all    │ 1204    │ 2026-09-20T22:41:19Z │
└──────────────────┴─────────────┴──────────┴────────────┴────────┴─────────┴──────────────────────┘
```

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:

```json lua drains list --json theme={null}
{
  "drains": [
    {
      "id": "drn_2b7f10d9ac4e83615702ffab",
      "orgId": "org_4f2c9a1b",
      "name": "Datadog prod",
      "type": "datadog",
      "endpoint": "https://http-intake.logs.datadoghq.eu/api/v2/logs",
      "site": "datadoghq.eu",
      "format": "json",
      "headers": [{ "name": "DD-API-KEY", "last4": "9f3c" }],
      "secretLast4": null,
      "selectors": {
        "sources": ["skill", "job", "webhook", "execution", "agent_error"],
        "minSeverity": "info",
        "agents": "*",
        "environments": ["production"],
        "includeContent": false
      },
      "state": "healthy",
      "verifiedAt": "2026-09-20T11:02:44.117Z",
      "health": {
        "lastSuccessAt": "2026-09-21T09:14:02.881Z",
        "backlog": 0,
        "backlogBytes": 0,
        "deliveredCount24h": 418223,
        "droppedCount24h": 0,
        "rejectedCount24h": 0,
        "p50LatencyMs": 138,
        "scrubHits24h": 17
      },
      "quota": {
        "eventsPerDay": 10000000,
        "bytesPerDay": 10737418240,
        "usedEvents": 418223,
        "usedBytes": 289417216,
        "resetsAt": "2026-09-22T00:00:00.000Z",
        "degradation": "none"
      },
      "version": 4,
      "createdAt": "2026-09-20T10:58:12.004Z",
      "updatedAt": "2026-09-21T09:14:02.881Z"
    }
  ]
}
```

`status --json` returns the same drains trimmed to what an operator watches:

```json lua drains status --json theme={null}
{
  "drains": [
    {
      "id": "drn_2b7f10d9ac4e83615702ffab",
      "name": "Datadog prod",
      "state": "healthy",
      "health": { "lastSuccessAt": "2026-09-21T09:14:02.881Z", "backlog": 0, "backlogBytes": 0,
                  "deliveredCount24h": 418223, "droppedCount24h": 0, "rejectedCount24h": 0,
                  "p50LatencyMs": 138, "scrubHits24h": 17 },
      "quota": { "eventsPerDay": 10000000, "bytesPerDay": 10737418240, "usedEvents": 418223,
                 "usedBytes": 289417216, "resetsAt": "2026-09-22T00:00:00.000Z", "degradation": "none" }
    }
  ]
}
```

`create --json` is the only place the signing secret appears. It is present for `http` drains, which are signed, and absent for the others:

```json lua drains create --json theme={null}
{
  "drain": { "id": "drn_9f31a7c04b2e615d8a03cc71", "name": "Log receiver", "type": "http",
             "endpoint": "https://logs.example.com/lua", "state": "pending_verification" },
  "secret": "whsec_7Qb3xA1mR8pK0sVnT4uZ2yL6cE9dW5gH"
}
```

`test --json` reports the round trip:

```json lua drains test --json theme={null}
{
  "batchId": "01JBWA6R1KD3P8N0SY4V2XQZ7T",
  "statusCode": 200,
  "latencyMs": 142,
  "responseExcerpt": "ok",
  "ok": true,
  "errorClass": null
}
```

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.

`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:

```bash theme={null}
lua drains create --ci --json \
  --name "Datadog prod" \
  --type datadog \
  --site datadoghq.eu \
  --header-from-env DD-API-KEY=DATADOG_API_KEY \
  --environments production \
  --min-severity info
```

Create a signed HTTPS drain, save the secret, and verify the endpoint:

```bash theme={null}
secret=$(lua drains create --ci --json \
  --name "Log receiver" --type http \
  --endpoint https://logs.example.com/lua | jq -r '.secret')

lua drains verify drn_9f31a7c04b2e615d8a03cc71
```

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:

```bash theme={null}
lua drains status --ci --json | jq -e '[.drains[] | select(.state == "failing")] | length == 0'
```

Look at why a drain is unhappy:

```bash theme={null}
lua drains deliveries drn_9f31a7c04b2e615d8a03cc71 --limit 20 --kind batch --json \
  | jq -r '.deliveries[] | select(.ok == false) | [.startedAt, .statusCode, .errorClass, .responseExcerpt] | @tsv'
```

Narrow a noisy drain to errors on one agent:

```bash theme={null}
lua drains update drn_9f31a7c04b2e615d8a03cc71 \
  --min-severity error \
  --agents agent_1789214224176_2vta8rnyn
```

Pause during a receiver deploy, then resume. Records buffer while it is paused:

```bash theme={null}
lua drains pause drn_9f31a7c04b2e615d8a03cc71 --reason "receiver deploy"
lua drains resume drn_9f31a7c04b2e615d8a03cc71
```

Rotate the signing secret with no delivery gap:

```bash theme={null}
lua drains rotate-secret drn_9f31a7c04b2e615d8a03cc71 --json | jq -r '.secret'
# deploy the new value alongside the old one, then:
lua drains rotate-secret drn_9f31a7c04b2e615d8a03cc71 --finalize
```

## Errors

A `4xx` with a code prints `<code>: <message>`, plus the offending field when the server names one.

| Code                         | Meaning                                                            |
| ---------------------------- | ------------------------------------------------------------------ |
| `DRAIN_VALIDATION_FAILED`    | A field is malformed; the `field` names it                         |
| `DRAIN_NOT_FOUND`            | No such drain in this organization                                 |
| `DRAIN_NAME_TAKEN`           | Another drain already has that name                                |
| `DRAIN_ROTATION_IN_PROGRESS` | A rotation window is open; finalize it or pass `--finalize`        |
| `DRAIN_LIMIT_REACHED`        | 5 drains already exist (10 on Enterprise)                          |
| `DRAIN_TYPE_UNAVAILABLE`     | That destination type is not available yet                         |
| `DRAIN_ENDPOINT_INVALID`     | Not HTTPS, or a private, loopback, link-local, or metadata address |
| `DRAIN_ENDPOINT_NOT_ALLOWED` | The host is not one this preset accepts                            |
| `DRAIN_CONTENT_ACK_REQUIRED` | `--include-content` without a confirmation or `--yes`              |
| `DRAIN_SCRUB_RULE_INVALID`   | An organization scrub rule fails the pattern limits                |
| `DRAIN_AGENT_NOT_IN_ORG`     | An id in `--agents` is not in this organization                    |
| `DRAIN_VERIFY_RATE_LIMITED`  | More than 5 verify attempts for this drain in an hour              |

A `403` prints:

```text theme={null}
✖ forbidden: Missing scope: logs:manage. Create a key with that scope (lua auth key) or ask an org admin.
```

## Exit codes

| Code | Meaning                                                                                                                                                   |
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `0`  | The action succeeded.                                                                                                                                     |
| `1`  | `test` got a non-`2xx` from the destination, `verify` did not verify, or the outcome had not landed inside the wait (30 s for `test`, 60 s for `verify`). |
| `2`  | Invalid usage — and, for `status`, at least one drain is in state `failing`, set after the output is printed.                                             |
| `3`  | The drain id does not exist.                                                                                                                              |
| `9`  | No credential, or the server rejected it.                                                                                                                 |
| `10` | The credential lacks `logs:manage`.                                                                                                                       |
| `11` | The server or network is unavailable.                                                                                                                     |

## See also

* [Ship logs to your stack](/drains/overview) — what a drain is, and what it selects
* [Delivery guarantees](/drains/delivery-guarantees) — retry, drops, heartbeat, and the health states behind `status`
* [Verify signatures](/drains/verify-signatures) — what `rotate-secret` changes on the wire
* [Log drains REST](/reference/rest/log-drains) — the same operations over HTTP
* [`lua logs`](/reference/cli/logs) — reading the same records from the platform's own copy
