Skip to main content
Available from lua-cli 3.39 and the platform release of 23 September 2026. Until that release lands, --sources workflow-audit is refused and no lua.workflow-audit.info record is produced. The date is the one in force as this is written and is subject to change until the release is announced — treat the version as the reliable half.
The workflow-audit source copies your organization’s workflow audit trail — who approved a run, who changed a policy, who exported what — into the same log drain as everything else. Selecting it is how an organization ships its audit to a SIEM from this release onwards, and it replaces the per-organization workflow SIEM webhook. workflow-audit is off by default, on a new drain and an existing one alike. It is a compliance stream you opt into:
Opting in has nothing to do with --include-content. workflow-audit is not a content source: selecting it never prints the content acknowledgement, and a drain that selects only it is not a drain that carries end-user messages. --include-content changes only which five attributes arrive.

One event name, not one per action

That is the whole vocabulary. An event name is lua.<source>.<suffix> with the suffix restricted to lowercase letters, and every audit action is dotted and underscored (workflow.run.step_parked), so an action could never be the suffix — and widening the name pattern would be a breaking schema change rather than an additive one. The action is an attribute. Switch on lua.audit.action, with a default branch, exactly as you would switch on lua.source rather than on a per-server MCP event name. New actions appear within this major version and must not break your ingest.

Every audit record is INFO

severityNumber is 9 on all of them. There is deliberately no severity ladder over the actions: a compliance sink wants the whole trail rather than the alarming part of it, and a second vocabulary to keep in step with the first is a source of drift. That has one consequence big enough to have its own row below: a drain whose --min-severity is warn or error receives no audit records at all, even though it selects the source.

Selectors that will silently deliver nothing

Three of a drain’s selectors can leave you with a correctly configured audit drain and an empty destination. There is no error and no delivery-log row — the records are simply never matched. --sampling is not a fourth trap, but it is not a volume knob here either: only debug records are sampleable and audit records are always info, so a sampling rule has no effect on this source.
Creating an audit drain inside a project directory sets --agents to that project’s agent rather than to the whole organization. For an audit drain that is almost never what you want — pass --agents all explicitly.

Attributes

There is no body on an audit record, with or without content.

lua.audit.metadata is a JSON string

Attribute values in this schema are scalars, and that does not change. Audit metadata is free-form per action, so flattening it would put unbounded per-action key names into every destination’s attribute index — a cost bug at the destination, not a convenience. Parse it at ingest:
Over the 4 KB cap the whole value is replaced by a marker rather than cut:
So an oversized entry is never dropped and never silently halved — and JSON.parse still succeeds, which is why the marker is an object rather than a string.

The content rule

The five attributes marked content only are free text about a person. They are withheld unless the drain has --include-content, and the record still arrives, carrying lua.content.withheld: true. Ids, the action, the target and the surface are never withheld, so an organization can always see that the action happened. lua.workflow-audit.info carries lua.content.withheld whenever content is off — even for an entry that recorded no evidence at all. That is deliberate: an entry with nothing to show must not be indistinguishable from one whose evidence you were not sent. Treat the flag as advisory and the presence of each key as authoritative. One audit entry produces a different record per drain: a drain with content on and a drain with content off in the same organization each receive the record they asked for, under the same record id. Content or not, every attribute passes the scrubber. An organization scrub rule that matches inside lua.audit.metadata masks it there exactly as it would in a log body.

Delivery, ordering and duplicates

The record id is the audit entry’s own id, so a retried write or a replayed sweep cannot double-deliver. Delivery is at-least-once like every drain: deduplicate on records[].id. Records are enqueued only after the audit entry is durable, so a drain never carries an entry the platform’s own audit trail does not have. The reverse is possible and intended: the drain is fire-and-forget, and a drain outage never fails the action that was audited. Your SIEM is a copy, not the system of record. Audit entries are written by more than one part of the platform, so the trail a drain carries is the whole one — run and ledger actions alongside read-access, policy, export and artefact actions. A newly created or newly edited drain starts (or stops) receiving audit within about 30 seconds.

Migrating from the workflow SIEM webhook

The per-organization workflow SIEM webhook (audit.siemWebhook on the workflow policy) is replaced by a drain that selects workflow-audit. It was a second delivery pipeline with its own signing, its own backoff and its own ordering, and none of the health, quota, delivery log or replay a drain already has. Existing webhooks are migrated for you. You do not have to create the drain.

What changes for your receiver

Every field of the old body is present on the new record, as an attribute — the table above maps them. The old action is lua.audit.action; the entry’s metadata is lua.audit.metadata, a JSON string. Verify signatures has the new format, working verifiers and published test vectors.

The overlap release

A drain created by the migration adds two extra headers to every delivery, for one release only:
<body> is the uncompressed batch body — the same bytes X-Lua-Signature covers. So a receiver written against the old webhook can keep its existing verification code pointed at X-Lua-Signature-Legacy and X-Lua-Timestamp while it moves to X-Lua-Signature, and the two headers can never disagree about which bytes were signed. The legacy value ships under its own header name because X-Lua-Signature is already the drain protocol’s header and the two values are not interchangeable: a different signed string and a different timestamp unit. Overwriting it would break every conforming verifier at once.
The legacy headers are removed in the release after this one. Move your verifier to X-Lua-Signature before then. They appear only on drains the migration created — there is no create or update option that turns them on, in the CLI or over the API, so a drain you make yourself is signed the one modern way from the start.

What to check after your organization is migrated

1

Verify the drain

The migration leaves the drain in pending_verification, which buffers rather than delivers. Prove ownership of the endpoint and it moves to healthy:
2

Watch the test record land

A lua.drain.test record is enqueued for you and delivered by the normal flush, not by the migration. Confirm it arrived rather than trusting that the migration finished:
3

Check the quota

The drain’s allowance is recomputed from your plan on its next write. Confirm it is not sitting at a zero allowance before you call it live:

Two things the migration changes on purpose

  • --include-content is turned on. Creating such a drain by hand would require the content acknowledgement first. The migration does not ask for one, because nothing is transitioning: the webhook had been delivering this organization’s audit metadata to this same endpoint under the policy you already set, and leaving the flag off would silently halve every record you receive. The drain selects only workflow-audit, so no end-user message content is in scope — but the flag is on, and adding a content source to that drain later will not re-ask.
  • You may receive more actions than before. A drain selector has no per-action filter: workflow-audit is the whole vocabulary. An organization whose webhook was narrowed to a subset of actions therefore receives more audit actions than before, never fewer. Plan for the ingest volume.

If your organization was not migrated

A migration run can refuse an organization rather than guess. The three reasons you can act on: The run is idempotent, so a re-run adopts the drain it already made rather than creating a second one.

The workflow policy field is deprecated

PATCH /admin/orgs/:orgId/workflow-policy with audit.siemWebhook still works in this release. The write happens; the notice is advisory, not a refusal. A request that names the field answers a deprecations entry in the body:
and the same thing in headers, in RFC 8594 shape, because an SDK reads JSON while a gateway or a linter reads headers:
A PATCH that does not name audit.siemWebhook carries neither the key nor the headers, so watching for Deprecation: true is enough to find the callers that still set it. Point new configuration at a drain instead.

See also

Ship logs to your stack

Every selectable source, and what a drain is.

Event schema

The full attribute catalogue and the content rule.

Verify signatures

The X-Lua-Signature format and working verifiers.

Delivery records

The other phase-2 source with its own page.