Skip to content

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

ConceptDescription
Audit EventAn immutable record of execution, security, or administrative activity.
RecordingPer-function toggle for workflow run, step, and saga audit capture.
Recording RetentionDeprecated per-function compatibility metadata; global retention applies uniformly.
Audit TrailThe 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 TypeWhenKey Payload FieldsGlobally redactable fields
run.createdA new run is createdrunId, functionId, input, eventIdinput
run.status_changedRun status transitionsrunId, oldStatus, newStatus, reasonreason
step.startedA step begins executionstepId, stepName, stepTypestepName
step.completedA step finishes successfullystepId, output, durationMsoutput
step.failedA step failsstepId, error, attempterror
step.patchedA step output is hot-patchedstepId, originalOutput, patchedOutput, patchedByoriginalOutput, patchedOutput
saga.compensation.startedA compensation step beginsstepId, compensationTargetStepNone
saga.compensation.completedA compensation step succeedsstepId, resultresult
saga.compensation.failedA compensation step failsstepId, error, attempterror
step.injectedStep output injected during scoped injectionstepId, previousOutput, newOutput, reasonpreviousOutput, newOutput, reason
run.pausedRun paused at step boundaryrunId, oldStatus, newStatus, reasonNone
run.resumedRun resumed after pauserunId, actorId, fromStepNone
run.cancelledA run was cancelled by an operatorrunId, actorId, reason, causereason
debounce.cancelledDebounce window closed without invocationeventId, functionId, debounceKeydebounceKey
environment.createdEnvironment createdenv_id, env_name, project_id, org_id, actor_id, change_typeNone
environment.deletedEnvironment deletedenv_id, env_name, project_id, org_id, actor_id, change_typeNone
environment.updatedEnvironment renamed or recoloredsame as aboveNone

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:

Terminal window
IRONFLOW_AUDIT_PAYLOAD_REDACT_FIELDS='run.created.input,step.completed.output' ironflow serve

The 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 TypeWhenKey Payload Fields
capacity.lease.expiredA worker lease expired and was fenced/reclaimedenvironment, executionSeq, laneDigest, leaseTokenHash, reason
capacity.segment.recoveredAn expired segment was re-enqueued under a fresh executionSeqenvironment, executionSeq, laneDigest, leaseTokenHash, reason
capacity.stale_mutation.rejectedA stale worker message was fenced offenvironment, executionSeq, laneDigest, leaseTokenHash, reason
capacity.refresh.rejectedA stale or unknown lease refresh was rejectedenvironment, executionSeq, laneDigest, leaseTokenHash, reason

laneDigest and leaseTokenHash are opaque digests. Raw keys and lease tokens never reach the payload.

Authorization

Event TypeWhenKey Payload Fields
authz.decision.allowedAn authorization check passedapi_key_id, action, resource, decision, environment, method, path, policy_id, reason
authz.decision.deniedAn authorization check failedsame as above
authz.key.createdAn API key was createdapi_key_id, key_name, org_id, actor_id
authz.key.revokedAn API key was revokedapi_key_id, key_name, org_id, actor_id, reason
authz.role.changedA role was created, updated, or deletedrole_id, role_name, org_id, change_type, actor_id
authz.policy.changedA CEL policy was created, updated, or deletedpolicy_id, policy_name, org_id, change_type, actor_id

Platform

Scoped platform, not tenant. Read them via /api/v1/platform/audit.

Event TypeWhenKey Payload Fields
platform.user.createdA platform user was createduser_id, email, change_type
platform.user.updatedA platform user was updateduser_id, email, change_type
platform.user.deletedA platform user was deleteduser_id, email, change_type
platform.impersonatedAn impersonation session was startedapi_key_id, platform_user_id, target_org_id, target_env_id, action, allowed
platform.role.changedA delete or update of a built-in role was blockedrole_id, role_name, action, change_type
platform.key.createdReserved — declared but not currently emitted
platform.key.revokedReserved — declared but not currently emitted
cluster.encryption.verifiedThe startup encryption pre-flight ranstatus, 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 TypeWhenKey Payload Fields
secret.createdA secret was createdname, org_id, env_id, actor_id, change_type
secret.updatedA secret’s value was replacedsame as above
secret.deletedA secret was deletedsame as above
kv.bucket.createdA KV bucket was createdbucket, key, org_id, env_id, actor_id, operation
kv.bucket.deletedA KV bucket was deletedsame as above
kv.key.putA KV key was writtensame as above
kv.key.deletedA KV key was deletedsame as above
webhook.source.createdA webhook source was provisionedsource_id, name, org_id, env_id, actor_id
cluster.token.rotatedA cluster cap-token was rotatedcluster_id, token_kind, actor_id, reason
org.exportA customer-data export completedorg_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 TypeWhenExtra Payload Fields
function.createdA function was registered for the first time
function.updatedA registered function’s config changed
function.deletedA function was deleted
function.pausedA function was paused
function.resumedA paused function was reactivated
function.rolled_backA function was rolled back to an earlier versionversion (the target)
projection.registeredA projection was registered or re-registered
projection.deletedA projection was unregistered
projection.pausedA projection stopped consuming events
projection.resumedA paused projection resumed consuming
projection.rebuild_startedA rebuild job was queued — not that it finished
projection.rebuild_cancelledA running rebuild job was cancelled
schema.registeredAn event schema version was registeredversion
schema.deletedAn event schema version was deletedversion
consumer_group.createdA consumer group was created
consumer_group.updatedA consumer group’s config changedfields (changed field names)
consumer_group.deletedA consumer group was deleted
deployment.registeredAn executable deployment was registeredversion (the artifact digest)
deployment.conflict_rejectedA registration tried to redefine an existing deployment ID and was refusedversion (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

DataPurpose and persistence
Execution auditOptional function-scoped lifecycle events for inspection, controlled by recording.
Durable step resultsPersisted outputs used to resume a run, even with recording: false.
Case stateApplication business facts, such as a ticket status, stored in the application’s chosen database or entity stream.
Projection-backed memory and curated knowledgeDerived 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:

ProfileCaptured workflow events
allRun lifecycle, step, and saga compensation events
run_lifecyclerun.created and run.status_changed
stepsStep events and saga compensation events
empty or omittedLegacy 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;
},
);

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.

EnvDefaultNotes
IRONFLOW_AUDIT_RETENTION_DAYS900 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

Terminal window
# View audit trail for a run
ironflow audit trail <run-id>
# Filter by event type
ironflow audit trail <run-id> --type step.completed
# Time range filtering
ironflow audit trail <run-id> --from 2025-01-01T00:00:00Z --to 2025-01-02T00:00:00Z
# Limit results
ironflow audit trail <run-id> --limit 100
# JSON output (for piping to jq, etc.)
ironflow audit trail <run-id> --json

Example output:

TIMESTAMP EVENT TYPE STEP ID PAYLOAD
10: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 events

REST API

Terminal window
# Get audit events for a specific run
curl -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 cursor
curl "http://localhost:9123/api/v1/audit?cursor=<next_cursor>&limit=50"

Query parameters:

ParameterDescription
function_idFilter by function ID
run_idFilter by run ID
event_typeFilter by event type
fromStart timestamp (RFC3339)
toEnd timestamp (RFC3339)
limitMaximum events to return (default: 100)
cursorPagination 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)
}
// Paginate
if result.NextCursor != "" {
nextPage, _ := client.GetAuditTrail(ctx, "run-abc123", ironflow.GetAuditTrailOpts{
Cursor: result.NextCursor,
})
// ...
}

Python SDK

from ironflow import IronflowClient, IronflowRPC
from 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 / Dashboard

Key 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 TypeWhenKey Payload Fields
authz.decision.allowedAuth middleware grants accessapi_key_id, action, resource, environment, method, path, decision, policy_id (conditional), impersonated_org_id (conditional)
authz.decision.deniedAuth middleware denies accessapi_key_id, action, resource, environment, method, path, reason, decision, policy_id (conditional), impersonated_org_id (conditional)
authz.key.createdAPI key createdapi_key_id, key_name, org_id, actor_id
authz.key.revokedAPI key deleted or rotatedapi_key_id, key_name, org_id, actor_id
authz.role.changedRole created, updated, or deletedrole_id, role_name, org_id, change_type, actor_id
authz.policy.changedPolicy created, updated, or deletedpolicy_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

Terminal window
# View auth audit trail for an organization
ironflow audit auth-trail --org <org_id>
# Filter by API key
ironflow audit auth-trail --org <org_id> --key <api_key_id>
# Filter by action
ironflow audit auth-trail --org <org_id> --action "functions:invoke"
# Time range
ironflow audit auth-trail --org <org_id> --from 2026-01-01T00:00:00Z --to 2026-01-02T00:00:00Z
# Limit results and JSON output
ironflow audit auth-trail --org <org_id> --limit 50 --json

Example output:

TIMESTAMP EVENT TYPE DECISION PAYLOAD
10: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 events

ConnectRPC API

The GetAuthAuditTrail RPC is available on the AuditService:

Terminal window
# Using buf curl (ConnectRPC)
buf curl --data '{"org_id": "org-123", "limit": 50}' \
http://localhost:9123/ironflow.v1.AuditService/GetAuthAuditTrail

Request fields:

FieldDescription
org_idRequired. Organization ID to query
api_key_idFilter by API key
actionFilter by action (e.g., functions:invoke)
from_timestampStart timestamp (RFC3339)
to_timestampEnd timestamp (RFC3339)
limitMaximum events to return (default: 100)
cursorPagination 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

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.

VariableDefaultBehavior
IRONFLOW_AUDIT_AUTHZ_CAPTURE_SUCCESSFUL_READStrueSet to false to omit allowed decisions for actions ending in :read or :list.
IRONFLOW_AUDIT_AUTHZ_EXCLUDE_ACTIONSemptyComma-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:

Terminal window
IRONFLOW_AUDIT_AUTHZ_CAPTURE_SUCCESSFUL_READS=false IRONFLOW_AUDIT_AUTHZ_EXCLUDE_ACTIONS='functions:list,runs:read' ironflow serve

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

Terminal window
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.jsonl

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