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; },);var ProcessOrder = ironflow.CreateFunction(ironflow.FunctionConfig{ ID: "process-order", Recording: true, RecordingRetention: "90d", 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})Retention Options
| Value | Description |
|---|---|
7d | Keep audit events for 7 days |
30d | Keep audit events for 30 days (default) — retention value is stored but automatic purging is not yet implemented |
90d | Keep audit events for 90 days |
forever | Never automatically delete audit events |
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 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 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, }) // ...}Metadata
Note: The
metadatacolumn 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 / 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 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