Audit Trail Schema
Reference documentation for audit trail JSONL entries. Each line in the audit trail is a complete JSON object.
Entry Structure
All entries follow a common envelope with event-specific fields added:
{
"event": "workflow.started | workflow.completed",
"execution_id": "unique-uuid-v4",
"timestamp": "2026-02-20T23:15:42.123+01:00",
"user": "operating-system-username",
"workflow_name": "workflow-name",
"schema_version": 1,
"...": "event-specific fields"
}Field Specifications
Common to All Entries
| Field | Type | Required | Example | Notes |
|---|---|---|---|---|
event | string | yes | "workflow.started" | Event type: one of workflow.started, workflow.completed |
execution_id | string | yes | "550e8400-e29b-41d4-a716-446655440000" | UUID v4, shared between paired start/completed entries for correlation |
timestamp | string | yes | "2026-02-20T23:15:42.123+01:00" | ISO 8601 format with millisecond precision and timezone offset |
user | string | yes | "deploy-bot" | OS username of effective user (resolves via user.Current().Username, fallback to USER env var, then "unknown") |
workflow_name | string | yes | "deploy-app" | Workflow name from YAML definition |
schema_version | integer | yes | 1 | Schema version for forward compatibility; currently always 1 |
workflow.started Event
Emitted when a workflow execution begins, capturing intent (before execution starts).
Additional Fields:
| Field | Type | Required | Example | Notes |
|---|---|---|---|---|
inputs | object | yes | {"env":"staging","api_key":"***"} | Key-value map of workflow inputs from --input flags. Secret-pattern keys masked as "***". If entry would exceed 4KB, input values are truncated progressively (longest first) and inputs_truncated flag set to true. |
inputs_truncated | boolean | no | true | Present only if input values were truncated to stay under 4KB limit |
Minimal Example:
{
"event": "workflow.started",
"execution_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-20T23:15:42.123+01:00",
"user": "deploy-bot",
"workflow_name": "deploy-app",
"inputs": {},
"schema_version": 1
}With Inputs:
{
"event": "workflow.started",
"execution_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-20T23:15:42.123+01:00",
"user": "deploy-bot",
"workflow_name": "deploy-app",
"inputs": {
"env": "staging",
"region": "us-east-1",
"api_key": "***",
"database_password": "***"
},
"schema_version": 1
}workflow.completed Event
Emitted when a workflow execution ends (success or failure), capturing outcome.
Additional Fields:
| Field | Type | Required | Example | Notes |
|---|---|---|---|---|
status | string | yes | "success" or "failure" | Final execution status |
exit_code | integer | yes | 0 (success) or non-zero (failure) | Process exit code |
duration_ms | integer | yes | 30333 | Elapsed milliseconds from execution start to completion |
error | string | no | "step 'deploy' failed: connection timeout" | Error description if status is "failure". Omitted if status is "success" (not empty string). |
Success Example:
{
"event": "workflow.completed",
"execution_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-20T23:16:12.456+01:00",
"user": "deploy-bot",
"workflow_name": "deploy-app",
"status": "success",
"exit_code": 0,
"duration_ms": 30333,
"schema_version": 1
}Failure Example:
{
"event": "workflow.completed",
"execution_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-20T23:16:12.456+01:00",
"user": "deploy-bot",
"workflow_name": "deploy-app",
"status": "failure",
"exit_code": 3,
"duration_ms": 30333,
"error": "step 'deploy' failed: connection timeout",
"schema_version": 1
}Completion Exit-Code Invariant
New workflow.completed entries use a consistent status/code pair:
status: "success"always hasexit_code: 0.status: "failure"always has a non-zeroexit_code.- Cancelled executions have audit status
failure. - An explicit non-zero code from the execution or failed step is preserved.
- An uncategorized failure, including a cancellation without a more precise code, uses exit code
3(execution error).
The execution history record and audit event receive the same normalized exit
code when both writes succeed. This applies to new records only; existing
failure/0 entries are not migrated.
Lifecycle Invariant
For every started execution attempt, AWF attempts one matching
workflow.completed write. This applies to success, step or operation
failure, cancellation, timeout, signal interruption, and recovered panic. The
completion event is derived from the final execution result before it is written;
an event with a non-empty error is therefore never serialized as success.
Field Constraints
execution_id
- Format: UUID v4 (RFC 4122)
- Uniqueness: One per workflow execution
- Correlation: Both
workflow.startedandworkflow.completedfor the same execution share the sameexecution_id - Usage:
jq 'select(.execution_id == "550e8400-e29b-41d4-a716-446655440000")' audit.jsonlto find all events for one execution
timestamp
- Format: ISO 8601 with timezone:
YYYY-MM-DDTHH:MM:SS.sssZ±HH:MM - Precision: Millisecond (3 decimal places for fractional seconds)
- Timezone: Always includes offset (e.g.,
+01:00,-05:00,Zfor UTC) - Examples:
2026-02-20T23:15:42.123+01:00,2026-02-20T04:15:42.000Z
user
- Derivation:
- Try
user.Current().Username(POSIX) - Fallback to
USERenvironment variable - Fallback to
"unknown"if both fail
- Try
- Behavior with sudo: Records the effective user (user after
sudo), not the original user who invokedsudo - Example: Running
sudo awf run workflowrecordsuser: "root"(the effective user)
inputs (masking rules)
Keys matching these patterns have values masked as "***":
SECRET_*(case-insensitive)API_KEY*(case-insensitive)PASSWORD*(case-insensitive)TOKEN*(case-insensitive)
Examples:
| Original Input | Masked Value |
|---|---|
api_key: "sk-1234567890abcdef" | "api_key": "***" |
APIKey: "sk-1234567890abcdef" | "APIKey": "***" |
secret_token: "ghp_abcdef123456" | "secret_token": "***" |
normal_input: "value" | "normal_input": "value" |
Entry Size (4KB Limit)
- Constraint: Each serialized JSONL line, including its newline, must fit within 4KB (4096 bytes) to be written
- Rationale: Keeps each record in one bounded append operation
- Truncation: If an entry would exceed 4KB:
- Input values are truncated progressively (longest values first)
inputs_truncated: trueflag is added to the entry- For completion events, an oversized error is shortened with an explicit
… [truncated]suffix, preserving terminal identity, status, and exit code - If the remaining identity fields still exceed the limit, the writer returns an error without writing a partial entry
Truncation Example:
{
"event": "workflow.started",
"execution_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": "2026-02-20T23:15:42.123+01:00",
"user": "deploy-bot",
"workflow_name": "deploy-app",
"inputs": {
"large_config": "[truncated: 8192 bytes]",
"api_key": "***"
},
"inputs_truncated": true,
"schema_version": 1
}Format Guarantees
JSONL (JSON Lines)
- One JSON object per line (delimited by
\n) - Each line is a complete, valid JSON object
- Lines are ordered chronologically (earliest first)
- No JSON array wrapper; enables append-only writes and
tail -f
Encoding
- Character encoding: UTF-8
- Line endings: LF (
\n) only, not CRLF - Special characters: All JSON control characters properly escaped (e.g., newlines as
\n, quotes as\")
Atomicity & Concurrency
- Each entry is serialized and written as one complete record while the writer protects its append operation.
- The file is opened with
O_APPEND, allowing independent AWF processes to append to the same trail without overwriting existing records. - Entries are bounded to 4 KiB. Completion errors are truncated when necessary to preserve the terminal record; oversized identity fields are rejected. Write failures are reported to the caller.
Backward Compatibility
Schema Versioning
The schema_version field enables forward compatibility:
- Current:
1 - Future: When schema changes (e.g., new fields added), version increments
- Consumers: Should branch on
schema_versionto handle multiple formats - Migration: No migration tools needed; old and new entries coexist in the same file
Example consumer logic:
switch entry.SchemaVersion {
case 1:
// Handle schema v1
case 2:
// Handle schema v2 (hypothetical)
}Field Evolution
- Adding new fields: Safe — old consumers ignore unknown fields (JSON standard)
- Removing fields: Unsafe — old format incompatible with new schema
- Renaming fields: Requires schema version bump
- Changing field types: Requires schema version bump
Querying Examples
Parse and Pretty-Print
jq '.' ~/.local/share/awf/audit.jsonlFilter by Status
# Failed executions only
jq 'select(.status == "failure")' ~/.local/share/awf/audit.jsonl
# Successful executions only
jq 'select(.status == "success")' ~/.local/share/awf/audit.jsonlGroup by Execution ID
# Verify paired entries (should be 2 per execution)
jq -s 'group_by(.execution_id) | map(length)' ~/.local/share/awf/audit.jsonl
# Extract pairs with durations
jq -s '
group_by(.execution_id)
| map(select(length == 2) | {
execution_id: .[0].execution_id,
workflow_name: .[0].workflow_name,
duration_ms: .[1].duration_ms,
status: .[1].status,
user: .[0].user
})
' ~/.local/share/awf/audit.jsonlDetect Incomplete Executions
# Orphaned start entries (no matching completed)
jq -s '
group_by(.execution_id)
| map(select(length == 1) | .[0])
| map({execution_id, workflow_name, timestamp, user})
' ~/.local/share/awf/audit.jsonlSee Also
- Audit Trail Guide - Configuration and usage guide
- ADR-010: Paired JSONL Audit Trail with Atomic Append
- ADR-011: Application-Layer Secret Masking for Audit Events