- Platform & Tenancy
- Audit Log
Audit Log
The Audit Log shows recorded execution, security, and administrative events in an
environment. Function recording controls workflow run, step, and saga capture.
Security and resource events are captured independently of that flag, subject to
their capture settings. For example, KV audit emission requires
IRONFLOW_AUDIT_KV_EMIT=1. Platform-scoped records are available in Platform Audit.
Browse the dashboard feed
The dashboard enables Hide successful authorization checks by default to keep
routine reads from crowding out activity. Uncheck it, or use Clear filters, to
see successful checks alongside other events. Selecting
authz.decision.allowed explicitly also turns off the hide control.
Category presets narrow the displayed events:
- Workflow execution: run, step, saga, and debounce events, including operator actions on runs and steps.
- Security: authorization and platform event families, within the endpoint’s existing scope.
- Administration and infrastructure: other event families, including function, deployment, resource, and capacity events.
The category and hide controls filter loaded rows. They do not change capture or remove stored records. Function, run, and event-type filters are sent to the audit query. Active filters remain visible above the table. Counts distinguish shown rows, loaded rows, and the total matching the server query. Use Load More to inspect older pages, including when every loaded row is hidden by display filters. The page does not search all older pages automatically.
Loading, request failures, an empty feed, and no matching results have separate messages. Retry repeats a failed request; a failed refresh keeps previously loaded rows visible. Authorization decision rows show action, actor, API key, resource, and decision. Expand any row to inspect its full payload and metadata.
If the environment has no recorded events, check capture configuration and retention as well as function recording. Enabling function recording does not control security or resource capture.
Key Concepts
| Concept | Description |
|---|---|
| Audit Event | An immutable record of execution, security, or administrative activity. |
| Recording | Per-function toggle for workflow run, step, and saga audit capture. |
| Recording Retention | Deprecated per-function compatibility metadata; global retention applies uniformly. |
| Audit Trail | The ordered sequence of audit events for a run, queryable by type and time range. |
Event Types
Every audit event has a event_type that identifies what happened. The types are
defined as AuditEventType constants in internal/store/models.go, which is the
single source of truth. The dashboard catalog is generated from those declarations
and their scope/availability metadata. The database does not constrain the column
(see ADR-0052).
The tenant event selector offers available tenant-scoped types, including
deployment.registered and deployment.conflict_rejected. Platform-only types,
including cluster.encryption.verified, belong in Platform Audit and are omitted
from tenant choices. cluster.token.rotated is tenant-scoped despite its prefix.
The backend enforces query scope even if a caller supplies a platform event type.
platform.key.created and platform.key.revoked remain reserved declarations:
there are no current producers for these event names. They are omitted from the
selector, and an older link selecting one is labeled as reserved. This does not
remove access to stored records or introduce new capture behavior.
For contributors: add an explicit AuditEventType string constant with a trailing
// audit: tenant available annotation, using platform or reserved where
appropriate. Check the producer’s storage scope rather than inferring it from the
name. Run make audit-catalog and commit the generated file. make audit-catalog-check
verifies parity without rewriting the file and tests the generator. This gate runs
in make ci and before make web. Availability describes producer support; actual
capture still depends on the applicable recording and operator settings.
Runs, steps, and lifecycle
| Event Type | When | Key Payload Fields | Globally redactable fields |
|---|---|---|---|
run.created | A new run is created | runId, functionId, input, eventId | input |
run.status_changed | Run status transitions | runId, oldStatus, newStatus, reason | reason |
step.started | A step begins execution | stepId, stepName, stepType | stepName |
step.completed | A step finishes successfully | stepId, output, durationMs | output |
step.failed | A step fails | stepId, error, attempt | error |
step.patched | A step output is hot-patched | stepId, originalOutput, patchedOutput, patchedBy | originalOutput, patchedOutput |
saga.compensation.started | A compensation step begins | stepId, compensationTargetStep | None |
saga.compensation.completed | A compensation step succeeds | stepId, result | result |
saga.compensation.failed | A compensation step fails | stepId, error, attempt | error |
step.injected | Step output injected during scoped injection | stepId, previousOutput, newOutput, reason | previousOutput, newOutput, reason |
run.paused | Run paused at step boundary | runId, oldStatus, newStatus, reason | None |
run.resumed | Run resumed after pause | runId, actorId, fromStep | None |
run.cancelled | A run was cancelled by an operator | runId, actorId, reason, cause | reason |
debounce.cancelled | Debounce window closed without invocation | eventId, functionId, debounceKey | debounceKey |
environment.created | Environment created | env_id, env_name, project_id, org_id, actor_id, change_type | None |
environment.deleted | Environment deleted | env_id, env_name, project_id, org_id, actor_id, change_type | None |
environment.updated | Environment renamed or recolored | same as above | None |
Global payload redaction
Set IRONFLOW_AUDIT_PAYLOAD_REDACT_FIELDS to a comma-separated list of exact
<event_type>.<field> selectors. The setting applies to the whole server
process and is read once at startup. Empty or unset configuration keeps full
payload capture. For example:
IRONFLOW_AUDIT_PAYLOAD_REDACT_FIELDS='run.created.input,step.completed.output' ironflow serveThe table above is the complete selector catalog. Combining each event type
with one field in its last column gives the accepted selectors. Nested paths,
wildcards, duplicates, unknown fields, and selectors for protected event
families are rejected before the server accepts traffic. Authorization,
platform, resource, control-plane, capacity, and scanner families keep their
existing capture rules. The cataloged run.cancelled.reason remains
selectable even though cancellation is an operator action.
Ironflow copies and removes selected top-level fields before it serializes or
writes the audit row. It records the canonical omitted field names as a sorted,
comma-separated audit_redacted_fields metadata value. The marker contains no
omitted values. debounce.cancelled.debounceKey is the public selector. It
removes the existing debounce_key field from the stored JSON and records
debounceKey in the marker.
The policy affects rows written after the configured process starts. It does not rewrite existing audit rows. Audit queries and exports omit the selected fields on affected rows and expose the metadata marker so consumers can tell redaction from a field the producer never supplied.
Redaction changes only the audit copy. Durable run and step inputs, outputs,
and errors remain available through execution-state APIs and continue to drive
retry, resume, and memoization. Time travel still requires
recordingProfile=all, but that profile cannot restore a globally redacted
value. Reconstructed snapshots omit the selected input, output, error, or patch
value, and GetStepOutputAt returns no value for a redacted output.
If runtime sanitization fails, Ironflow skips that audit row, records a delivery failure, and logs the event type. The workflow and its durable state continue. Ironflow never falls back to the unsanitized payload.
Capacity and dispatch
Exception events only — normal queue/reserve/release transitions are metrics and logs, never audit rows.
| Event Type | When | Key Payload Fields |
|---|---|---|
capacity.lease.expired | A worker lease expired and was fenced/reclaimed | environment, executionSeq, laneDigest, leaseTokenHash, reason |
capacity.segment.recovered | An expired segment was re-enqueued under a fresh executionSeq | environment, executionSeq, laneDigest, leaseTokenHash, reason |
capacity.stale_mutation.rejected | A stale worker message was fenced off | environment, executionSeq, laneDigest, leaseTokenHash, reason |
capacity.refresh.rejected | A stale or unknown lease refresh was rejected | environment, executionSeq, laneDigest, leaseTokenHash, reason |
laneDigest and leaseTokenHash are opaque digests. Raw keys and lease tokens never reach the payload.
Authorization
| Event Type | When | Key Payload Fields |
|---|---|---|
authz.decision.allowed | An authorization check passed | api_key_id, action, resource, decision, environment, method, path, policy_id, reason |
authz.decision.denied | An authorization check failed | same as above |
authz.key.created | An API key was created | api_key_id, key_name, org_id, actor_id |
authz.key.revoked | An API key was revoked | api_key_id, key_name, org_id, actor_id, reason |
authz.role.changed | A role was created, updated, or deleted | role_id, role_name, org_id, change_type, actor_id |
authz.policy.changed | A CEL policy was created, updated, or deleted | policy_id, policy_name, org_id, change_type, actor_id |
Platform
Scoped platform, not tenant. Read them via /api/v1/platform/audit.
| Event Type | When | Key Payload Fields |
|---|---|---|
platform.user.created | A platform user was created | user_id, email, change_type |
platform.user.updated | A platform user was updated | user_id, email, change_type |
platform.user.deleted | A platform user was deleted | user_id, email, change_type |
platform.impersonated | An impersonation session was started | api_key_id, platform_user_id, target_org_id, target_env_id, action, allowed |
platform.role.changed | A delete or update of a built-in role was blocked | role_id, role_name, action, change_type |
platform.key.created | Reserved — declared but not currently emitted | — |
platform.key.revoked | Reserved — declared but not currently emitted | — |
cluster.encryption.verified | The startup encryption pre-flight ran | status, backend, required, data_path, reason, cluster_id, node_id |
cluster.encryption.verified is platform-scoped rather than tenant-scoped because its payload
describes the host, not an application: filesystem paths, cluster ID and node ID. It has no
actor — the engine emits it at startup.
Resources
| Event Type | When | Key Payload Fields |
|---|---|---|
secret.created | A secret was created | name, org_id, env_id, actor_id, change_type |
secret.updated | A secret’s value was replaced | same as above |
secret.deleted | A secret was deleted | same as above |
kv.bucket.created | A KV bucket was created | bucket, key, org_id, env_id, actor_id, operation |
kv.bucket.deleted | A KV bucket was deleted | same as above |
kv.key.put | A KV key was written | same as above |
kv.key.deleted | A KV key was deleted | same as above |
webhook.source.created | A webhook source was provisioned | source_id, name, org_id, env_id, actor_id |
cluster.token.rotated | A cluster cap-token was rotated | cluster_id, token_kind, actor_id, reason |
org.export | A customer-data export completed | org_id, actor_id, started_at, completed_at, include, record_counts, body_sha256, bytes_streamed |
Control-plane resources
Every row below shares one payload shape: resource_type, resource_id, name, org_id, env_id, actor_id, change_type, plus version and fields where they apply.
| Event Type | When | Extra Payload Fields |
|---|---|---|
function.created | A function was registered for the first time | — |
function.updated | A registered function’s config changed | — |
function.deleted | A function was deleted | — |
function.paused | A function was paused | — |
function.resumed | A paused function was reactivated | — |
function.rolled_back | A function was rolled back to an earlier version | version (the target) |
projection.registered | A projection was registered or re-registered | — |
projection.deleted | A projection was unregistered | — |
projection.paused | A projection stopped consuming events | — |
projection.resumed | A paused projection resumed consuming | — |
projection.rebuild_started | A rebuild job was queued — not that it finished | — |
projection.rebuild_cancelled | A running rebuild job was cancelled | — |
schema.registered | An event schema version was registered | version |
schema.deleted | An event schema version was deleted | version |
consumer_group.created | A consumer group was created | — |
consumer_group.updated | A consumer group’s config changed | fields (changed field names) |
consumer_group.deleted | A consumer group was deleted | — |
deployment.registered | An executable deployment was registered | version (the artifact digest) |
deployment.conflict_rejected | A registration tried to redefine an existing deployment ID and was refused | version (the rejected digest) |
deployment.registered is written only by the call that actually created the
row. A worker fleet re-registers the same build on every boot, and those
idempotent calls are not audited — otherwise the one event worth seeing would be
buried under one row per worker per rollout.
deployment.conflict_rejected is the opposite case, and it records something
that did not happen: an authorized caller tried to point an existing
deployment ID at a different artifact, snapshot, mode or endpoint, and the
server refused. Deployment IDs are what pinned runs resolve through, so
redefining one would move those runs onto code nobody chose for them.
Authorization logs allow/deny decisions and this call is allowed — the handler
is what rejects it — so this is the only record that the attempt was made. Its
version carries the digest that was refused; the one on record is readable
from the deployment itself.
A re-registration writes projection.registered / schema.registered again rather than an .updated variant: both resources are addressed by name (and version, for schemas), so registering over one is a registration that clobbers — and the clobber is the thing worth seeing.
Function code, schema documents and consumer-group configs never appear in these payloads. As with secrets, the event struct has no field to hold them.
Secret values, KV values, and token material are structurally absent from these payloads — omitted at the event-struct level, not filtered at serialization.
The four kv.* events are off by default. Set IRONFLOW_AUDIT_KV_EMIT=1 to record them; they sit on a hot data path and will grow the audit table quickly.
Effective settings and delivery health
The Settings page shows the effective policy for the current server. It
distinguishes per-function workflow recording from server-wide operator
controls: successful authorization capture, excluded allowed actions, KV
capture, global payload redaction, global retention, and delivery health. The
same read-only data is available at GET /api/v1/audit/settings.
payload_redaction_enabled reports whether the policy is active, and
payload_redaction_fields contains its normalized, sorted selectors.
Server-wide values are read at startup and changes require a restart; the view
does not offer edits.
Audit delivery is synchronous and best effort. A recorder attempts each row
once and logs a store error if that write fails. The request or workflow that
emitted the record continues, so telemetry failure does not become execution
failure. Automatic retries are disabled to avoid duplicate rows when a store
write result is ambiguous. A later successful audit write marks delivery
healthy again. Operators can alert on
ironflow_audit_write_failures_total{source,event_type} and on the audit
warning returned by /ready while delivery is degraded. The warning is
report-only so an idle replica remains available to recover after the store
comes back.
Audit and agent data boundaries
| Data | Purpose and persistence |
|---|---|
| Execution audit | Optional function-scoped lifecycle events for inspection, controlled by recording. |
| Durable step results | Persisted outputs used to resume a run, even with recording: false. |
| Case state | Application business facts, such as a ticket status, stored in the application’s chosen database or entity stream. |
| Projection-backed memory and curated knowledge | Derived state over explicit events. The application chooses what is suitable to retain and reuse as model context. |
The Node agent’s llm.complete wrapper persists the completion returned by the provider closure as a durable step result, including returned metadata. Disabling audit recording does not prevent that persistence. Return only replay-required data from the closure and omit unnecessary raw or sensitive content.
Raw audit logs are not automatically appropriate or authorized model context. Projections derive views; they do not automatically provide curation, redaction, or authorization. Apply those policies before supplying data to a model.
Audit retention and payload redaction govern audit rows, not all execution
data. Pruning or redacting audit events does not remove durable step results,
case state, or memory. Per-function recordingRetention is deprecated
compatibility metadata; the global audit pruner still applies as described
below.
Enabling Recording
Recording is configured per-function. When recording is off, no function-scoped audit events are captured. Existing events are preserved.
Use recordingProfile when a function needs a narrower workflow capture boundary:
| Profile | Captured workflow events |
|---|---|
all | Run lifecycle, step, and saga compensation events |
run_lifecycle | run.created and run.status_changed |
steps | Step events and saga compensation events |
| empty or omitted | Legacy behavior: recording: true means all; false means off |
A non-empty profile enables recording even when the legacy boolean is omitted. The
server returns the effective profile, so legacy enabled functions appear as
all. Operator, authorization, resource, control-plane, and scanner events have
their existing capture rules and are independent of this workflow profile.
Operator actions against the control plane also ignore it: run.cancelled, run.resumed, and every event in the Resources and Control-plane resources tables. They are not function-scoped application telemetry, so a per-function toggle must not be able to hide them.
Two more events ignore this toggle: capacity.lease.expired and capacity.segment.recovered. They are emitted by the internal capacity scanner, not by a function, and record unconditionally — they are platform concerns a tenant’s per-function toggle must not suppress. Everything else in the tables above respects the setting.
import { createFunction } from "@ironflow/node";
const processOrder = createFunction( { id: "process-order", recordingProfile: "steps", triggers: [{ event: "order.placed" }], }, async ({ event, step }) => { const validated = await step.run("validate", async () => { return validateOrder(event.data); }); // Step and saga compensation events are recorded; run lifecycle events are not. return validated; },);var ProcessOrder = ironflow.CreateFunction(ironflow.FunctionConfig{ ID: "process-order", RecordingProfile: ironflow.RecordingProfileSteps, Triggers: []ironflow.Trigger{{Event: "order.placed"}},}, func(ctx ironflow.Context) (any, error) { result, err := ironflow.Run(ctx, "validate", func() (any, error) { var data OrderData if err := ctx.Event.Data(&data); err != nil { return nil, err } return validateOrder(data) }) return result, err})Deprecated function retention
recordingRetention remains accepted as compatibility metadata. Values such as
7d, 30d, 90d, and forever do not control deletion and should be removed
from new function definitions. The server-wide pruner applies uniformly by age.
A legacy forever value does not exempt rows. Set
IRONFLOW_AUDIT_RETENTION_DAYS=0 to disable automatic audit pruning globally.
Global retention TTL
Independent of per-function metadata, the server runs a daily pruner that deletes rows older than IRONFLOW_AUDIT_RETENTION_DAYS from the audit_events table. This protects single-VPS deployments from unbounded table growth.
| Env | Default | Notes |
|---|---|---|
IRONFLOW_AUDIT_RETENTION_DAYS | 90 | 0 disables the pruner. Values between 1 and 6 are rejected at boot (safety floor is 7). |
The pruner runs once per day at 03:00 UTC and logs rows_deleted and duration on every pass. Multi-node clusters fire at the same wall-clock instant regardless of host TZ.
Querying the Audit Trail
Dashboard
Navigate to Audit Log in the left sidebar to browse audit events across all runs. You can filter by:
- Function — select a specific function from the dropdown
- Event type — filter by
step.completed,run.created, etc. - Run ID — search for events from a specific run
From any run detail page, click the Audit Log button to jump directly to that run’s audit events.
CLI
# View audit trail for a runironflow audit trail <run-id>
# Filter by event typeironflow audit trail <run-id> --type step.completed
# Time range filteringironflow audit trail <run-id> --from 2025-01-01T00:00:00Z --to 2025-01-02T00:00:00Z
# Limit resultsironflow audit trail <run-id> --limit 100
# JSON output (for piping to jq, etc.)ironflow audit trail <run-id> --jsonExample output:
TIMESTAMP EVENT TYPE STEP ID PAYLOAD10:23:01.123 run.created {"runId":"run-abc","functionId":"process-order"...}10:23:01.145 step.started step-1 {"stepId":"step-1","stepName":"validate","stepType":"invoke"}10:23:01.312 step.completed step-1 {"stepId":"step-1","output":{"valid":true},"durationMs":167}10:23:01.315 step.started step-2 {"stepId":"step-2","stepName":"charge","stepType":"invoke"}10:23:01.520 step.completed step-2 {"stepId":"step-2","output":{"charged":true},"durationMs":205}10:23:01.522 run.status_changed {"runId":"run-abc","oldStatus":"running","newStatus":"completed"}
Total: 6 eventsREST API
# Get audit events for a specific runcurl -H "Content-Type: application/json" -d '{"runId":"<run-id>"}' \ http://localhost:9123/ironflow.v1.AuditService/GetAuditTrail
# List audit events across all runs (with optional filters)curl "http://localhost:9123/api/v1/audit?function_id=process-order&event_type=step.failed&limit=50"
# Paginate with cursorcurl "http://localhost:9123/api/v1/audit?cursor=<next_cursor>&limit=50"Query parameters:
| Parameter | Description |
|---|---|
function_id | Filter by function ID |
run_id | Filter by run ID |
event_type | Filter by event type |
from | Start timestamp (RFC3339) |
to | End timestamp (RFC3339) |
limit | Maximum events to return (default: 100) |
cursor | Pagination cursor from previous response |
Go SDK
client := ironflow.NewClient(ironflow.ClientConfig{ ServerURL: "http://localhost:9123",})
result, err := client.GetAuditTrail(ctx, "run-abc123", ironflow.GetAuditTrailOpts{ EventType: "step.completed", Limit: 50,})if err != nil { log.Fatal(err)}
for _, event := range result.Events { fmt.Printf("%s %s %v\n", event.CreatedAt, event.EventType, event.Payload)}
// Paginateif result.NextCursor != "" { nextPage, _ := client.GetAuditTrail(ctx, "run-abc123", ironflow.GetAuditTrailOpts{ Cursor: result.NextCursor, }) // ...}Python SDK
from ironflow import IronflowClient, IronflowRPCfrom ironflow.rpc.v1 import GetAuditTrailRequest
with IronflowRPC(server_url="http://localhost:9123") as rpc: result = rpc.audit.get_trail(GetAuditTrailRequest( run_id="run-abc123", event_type="step.completed", limit=50, )) for event in result.events: payload = event.payload_value.to_python() if event.payload_value is not None else event.payload.to_python() print(event.created_at, event.event_type, payload) if result.next_cursor: next_page = rpc.audit.get_trail(GetAuditTrailRequest( run_id="run-abc123", cursor=result.next_cursor, ))
# The global feed remains REST-backed.client = IronflowClient(server_url="http://localhost:9123")client.audit_list(function_id="process-order", event_type="step.failed", from_="2026-01-01T00:00:00Z", limit=50)Metadata
The metadata column contains recorder-owned context. When the global payload
policy removes fields, audit_redacted_fields contains their canonical public
names as a sorted, comma-separated string. It never contains the removed
values. There is no user-facing API for attaching custom metadata to an audit
event.
How It Works
The audit recorder is integrated directly into the Ironflow engine. When a function has recording: true, the engine writes an audit event to the database at each lifecycle point — run creation, status transitions, step starts, completions, failures, hot-patches, and saga compensations.
Event triggers run │ ▼┌─────────────────────────────────┐│ Engine executes function ││ ││ run.created ──► audit_events ││ step.started ──► audit_events ││ step.completed ──► audit_events││ run.status_changed ──► ... │└─────────────────────────────────┘ │ ▼ Query via API / CLI / DashboardKey characteristics:
- Append-only — events are never modified or deleted (except by retention policy)
- Non-blocking — audit write failures are logged but never block workflow execution
- Time-ordered IDs — audit events use ULID identifiers for natural time ordering
- Per-function caching — the engine caches each function’s recording status to avoid repeated database lookups
Auth Audit Trail
In addition to workflow audit events, Ironflow can record an audit trail of every authorization decision and auth-related mutation. This is useful for compliance, debugging access issues, and security auditing.
Auth Event Types
| Event Type | When | Key Payload Fields |
|---|---|---|
authz.decision.allowed | Auth middleware grants access | api_key_id, action, resource, environment, method, path, decision, policy_id (conditional), impersonated_org_id (conditional) |
authz.decision.denied | Auth middleware denies access | api_key_id, action, resource, environment, method, path, reason, decision, policy_id (conditional), impersonated_org_id (conditional) |
authz.key.created | API key created | api_key_id, key_name, org_id, actor_id |
authz.key.revoked | API key deleted or rotated | api_key_id, key_name, org_id, actor_id |
authz.role.changed | Role created, updated, or deleted | role_id, role_name, org_id, change_type, actor_id |
authz.policy.changed | Policy created, updated, or deleted | policy_id, policy_name, org_id, change_type, actor_id |
Auth events use run_id="" and function_id="" since they are not workflow-scoped. The metadata JSON includes org_id and api_key_id (for decision events) for filtering. actor_id lives in the payload.
Querying Auth Audit Trail
CLI
# View auth audit trail for an organizationironflow audit auth-trail --org <org_id>
# Filter by API keyironflow audit auth-trail --org <org_id> --key <api_key_id>
# Filter by actionironflow audit auth-trail --org <org_id> --action "functions:invoke"
# Time rangeironflow audit auth-trail --org <org_id> --from 2026-01-01T00:00:00Z --to 2026-01-02T00:00:00Z
# Limit results and JSON outputironflow audit auth-trail --org <org_id> --limit 50 --jsonExample output:
TIMESTAMP EVENT TYPE DECISION PAYLOAD10:23:01.000 authz.decision.allowed allowed {"api_key_id":"key-1","action":"functions:invoke"...}10:23:02.000 authz.decision.denied denied {"api_key_id":"key-2","action":"admin:manage","reason":"..."}10:24:00.000 authz.role.changed {"role_id":"role-1","change_type":"created"...}
Total: 3 eventsConnectRPC API
The GetAuthAuditTrail RPC is available on the AuditService:
# Using buf curl (ConnectRPC)buf curl --data '{"org_id": "org-123", "limit": 50}' \ http://localhost:9123/ironflow.v1.AuditService/GetAuthAuditTrailRequest fields:
| Field | Description |
|---|---|
org_id | Required. Organization ID to query |
api_key_id | Filter by API key |
action | Filter by action (e.g., functions:invoke) |
from_timestamp | Start timestamp (RFC3339) |
to_timestamp | End timestamp (RFC3339) |
limit | Maximum events to return (default: 100) |
cursor | Pagination cursor from previous response |
Configuration
Auth audit logging writes captured decisions synchronously to the audit_events table. Write failures are logged but do not fail the request.
By default, all decisions are captured. Operators can suppress successful reads or specific allowed actions through the authorization capture policy. Denied decisions remain captured.
What’s Next?
- Time-Travel Debugging — audit events power time-travel debugging, letting you inspect run state at any historical point in time
- Workflows — learn about functions, steps, and execution modes
- Sagas — compensation patterns that generate
saga.compensation.*audit events - Debugging — use the TUI debugger alongside the audit trail
Investigation filters and shared links
The tenant Audit Log supports inclusive From time and To time bounds,
entered in UTC, an exact Authorization action such as functions:list, and
an exact API key ID. Enter the key identifier, never its secret. These filters
combine with function, run, and event types and apply to every page. Tenant
queries always exclude platform records.
Filters are stored in the URL. Reloading, sharing the URL, or using browser back/forward restores the selected query and display filters. Clear filters shows the complete feed, including successful checks. Display categories and successful-check hiding still apply only to loaded pages.
GET /api/v1/audit accepts from, to, action, and api_key_id alongside
function_id, run_id, event_type, limit, and cursor. Time bounds accept
RFC3339 timestamps or YYYY-MM-DD. A date alone means midnight UTC, including
the to bound; use a timestamp to include the rest of that day. Invalid bounds
return HTTP 400. Pagination preserves the filtered total count.
The response includes retention_days, the effective policy loaded at server
startup. The dashboard shows it with the results. Zero means global pruning is
disabled; a positive value makes older records eligible for the daily prune.
This policy applies to all audit records, including functions with a legacy
recordingRetention: "forever". The deprecated field remains accepted for
compatibility and does not override the global policy.
Authorization capture policy
Operators can configure capture through process environment variables. These settings apply across the server’s environments, with both YAML and flag-based startup, and require restarting every server process after a change.
| Variable | Default | Behavior |
|---|---|---|
IRONFLOW_AUDIT_AUTHZ_CAPTURE_SUCCESSFUL_READS | true | Set to false to omit allowed decisions for actions ending in :read or :list. |
IRONFLOW_AUDIT_AUTHZ_EXCLUDE_ACTIONS | empty | Comma-separated exact action names whose allowed decisions are omitted. Whitespace is trimmed; wildcards are not expanded. |
For example, start with successful-read capture disabled and an explicit action exclusion:
IRONFLOW_AUDIT_AUTHZ_CAPTURE_SUCCESSFUL_READS=false IRONFLOW_AUDIT_AUTHZ_EXCLUDE_ACTIONS='functions:list,runs:read' ironflow serveDenied decisions are always recorded, including for actions covered by either suppression setting. Key lifecycle, resource changes, and workflow recording are unaffected. Invalid boolean values fail startup. The defaults preserve previous capture volume; the dashboard’s Hide successful authorization checks control only changes display and never removes stored records.
Export the filtered Audit Log
Choose Export JSONL in Audit Log to download every matching page, including pages you have not loaded. The export preserves the selected environment, function, run, event types, inclusive time bounds, authorization action, API key ID, category, and Hide successful authorization checks setting. Changing filters while an export runs affects the page and future exports. It does not change the export in progress. Cancel export stops the download.
The server fixes the upper time bound at export start, or at your selected To time if earlier. Records timestamped after that bound are excluded. This is a timestamp-bounded scan, not a database snapshot: an earlier-timestamped record committed during export may be included if visible before its page is read. Retention pruning may remove records before the export reads them. The file’s manifest records the effective filters, environment, and cutoff.
The .jsonl file contains a manifest line, one record envelope per audit event
with the complete stored event under data, optional heartbeat lines, and a
completion footer. Payloads and metadata retain their stored structure and
redaction. The footer includes counts.audit_events and body_sha256, a SHA-256
of all bytes before the footer. The dashboard checks the footer and record count
before saving; it does not independently verify the checksum. An interrupted or
failed request shows an error and offers no partial file. Retry starts a new
export with the current filters and a new cutoff.
The dashboard holds the download in browser memory until it completes. Use a narrower time range for large exports, or consume the HTTP stream directly:
curl --fail-with-body \ -H "Authorization: Bearer $IRONFLOW_API_KEY" \ -H 'X-Ironflow-Environment: env_default' \ 'http://localhost:9123/api/v1/audit/export?category=workflow&hide_successful=true&from=2026-09-01T00:00:00Z&to=2026-09-10T23:59:59Z' \ -o audit-export.jsonlGET /api/v1/audit/export requires the same runs:read permission as Audit Log.
It exports tenant records from the request environment; query parameters cannot
select another environment or platform scope. It accepts the list endpoint’s
function_id, run_id, comma-separated event_type, from, to, action, and
api_key_id filters, plus category=all|workflow|security|administration and
hide_successful=true|false. Direct API requests default to all categories with
successful checks included. from and to accept RFC3339 timestamps or
YYYY-MM-DD for midnight UTC. cursor and limit are ignored so every matching
page is exported.
HTTP errors before streaming use an error status. A failure after streaming
starts uses a JSONL error envelope instead of the completion footer, so HTTP
200 alone does not prove completion. Direct API consumers must require a footer
whose count matches the event records; verify its checksum when checking file
integrity. See Exporting data for the shared
JSONL envelope format.