Skip to content

Audit Log

In a Continuous History system, the audit trail was always there — it’s not a separate feature, it’s inherent. Ironflow captures every state mutation as an immutable recorded fact: run creation, status change, step start, step completion, step failure, history correction (hot-patch), and saga compensation. This is history inspection — querying what your system recorded.

Key Concepts

Concept Description
Audit Event An immutable record of a single state mutation in a workflow run.
Recording Per-function toggle that controls whether audit events are captured.
Recording Retention Per-function retention period for audit events (7d, 30d, 90d, forever).
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:

Event Type When Key Payload Fields
run.created A new run is created runId, functionId, input, eventId
run.status_changed Run status transitions runId, oldStatus, newStatus, reason
step.started A step begins execution stepId, stepName, stepType
step.completed A step finishes successfully stepId, output, durationMs
step.failed A step fails stepId, error, attempt
step.patched A step output is hot-patched stepId, originalOutput, patchedOutput, patchedBy
saga.compensation.started A compensation step begins stepId, compensationTargetStep
saga.compensation.completed A compensation step succeeds stepId, result
saga.compensation.failed A compensation step fails stepId, error, attempt
step.injected Step output injected during scoped injection stepId, previousOutput, newOutput, reason
run.paused Run paused at step boundary runId, oldStatus, newStatus, reason
run.resumed Run resumed after pause runId, oldStatus, newStatus, reason
debounce.cancelled Debounce window closed without invocation eventId, functionId, debounceKey
environment.created Environment created env_id, env_name, project_id, org_id, actor_id, change_type
environment.deleted Environment deleted env_id, env_name, project_id, org_id, actor_id, change_type

Enabling Recording

Recording is configured per-function. When recording is off, no audit events are captured. Existing events are preserved.

import { createFunction } from "@ironflow/node";
const processOrder = createFunction(
{
id: "process-order",
recording: true,
recordingRetention: "90d",
triggers: [{ event: "order.placed" }],
},
async ({ event, step }) => {
const validated = await step.run("validate", async () => {
return validateOrder(event.data);
});
// Every step is recorded in the audit trail
return validated;
},
);

Retention Options

Value Description
7d Keep audit events for 7 days
30d Keep audit events for 30 days (default) — per-function retention metadata; informational only today
90d Keep audit events for 90 days
forever Per-function retention metadata; informational only today

Important: per-function recordingRetention is currently metadata only — the server-wide pruner deletes rows uniformly by age. A forever per-function value does not exempt that function’s audit rows from the global TTL. Set IRONFLOW_AUDIT_RETENTION_DAYS=0 to disable pruning entirely if any function must retain forever.

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

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 http://localhost:9123/api/v1/runs/<run-id>/audit
# 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:

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)
}
// Paginate
if result.NextCursor != "" {
nextPage, _ := client.GetAuditTrail(ctx, "run-abc123", ironflow.GetAuditTrailOpts{
Cursor: result.NextCursor,
})
// ...
}

Metadata

Note: The metadata column on audit events is reserved for future use. There is currently no user-facing API to attach custom metadata to individual audit events.


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

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:

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 every authorization decision synchronously to the audit_events table. Write failures are logged but do not block the response path.

Verbosity filtering (ExcludeActions) is supported internally but not yet exposed through CLI flags or configuration.


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