Audit events
Every workflow action emits a structured JSON audit event. You don’t add logging — it’s already there. The gateway records what happened, when, and who did it, automatically.
Configuration
Section titled “Configuration”audit: sink: stderrFour sink options:
| Sink | Behavior |
|---|---|
stderr | Writes one JSON line per event to stderr. Default. |
file | Writes one JSON line per event into a date-rotated directory of .log files. |
memory | Stores events in memory. Useful for testing. |
none | Drops all events. Disables auditing. |
File sink
Section titled “File sink”audit: sink: file path: ./audit/ rotation: dailypath is a directory, not a single file. The gateway creates it on first write if it doesn’t exist, then writes one JSON line per event into date-stamped .log files inside it.
Events are split into two categories per rotation window:
{stamp}-transitions.log— events withevent_type == "workflow.transition".{stamp}-audit.log— every other event.
audit.rotation controls the stamp granularity (defaults to daily):
| Value | Stamp format | Example file |
|---|---|---|
hourly | YYYY-MM-DD-HH | 2025-06-15-14-audit.log |
daily (default) | YYYY-MM-DD | 2025-06-15-audit.log |
weekly | YYYY-Www (ISO week) | 2025-W24-audit.log |
Each write appends and flushes, so the trail survives a crash. Because rotation is built in, you don’t need an external rotator like logrotate.
Memory sink
Section titled “Memory sink”audit: sink: memoryKeeps events in process memory. Useful in tests where you want to assert on what events were emitted without touching the filesystem.
Event shape
Section titled “Event shape”Every event has the same structure:
{ "id": "evt_a1b2c3d4e5f6789012345678abcdef01", "timestamp": "2025-06-15T14:32:08.123456Z", "workflow_id": "wf_d4e5f6a1b2c3", "correlation_id": "cor_f6a1b2c3d4e5789012345678abcdef01", "actor": "agent@default", "event_type": "workflow.started", "payload": {}}| Field | Type | Description |
|---|---|---|
id | string | Unique event ID (evt_ prefix + UUID) |
timestamp | string | ISO 8601 timestamp (UTC) |
workflow_id | string or null | The workflow instance this event belongs to |
correlation_id | string | Groups related events in the same operation (cor_ prefix + UUID) |
actor | string or null | Who triggered the action (principal subject) |
event_type | string | What happened (see below) |
payload | object | Event-specific data |
trace_id | string (optional) | Caller-supplied trace ID spanning multiple workflows in one logical operation. Opaque to the gateway; omitted when absent. |
run_id | string (optional) | Caller-supplied ID for grouping related workflow instances. Opaque to the gateway; omitted when absent. |
Event types
Section titled “Event types”Workflow lifecycle
Section titled “Workflow lifecycle”| Event type | When it fires |
|---|---|
workflow.started | A new workflow instance is created via flowgate.command({definitionId}) |
workflow.transition | A transition is applied to the workflow (the full-weight record) |
workflow.interaction | A lightweight transition is applied (a transition marked lightweight) |
workflow.transitioned | A workflow successfully moves to a new state |
workflow.cancelled | A workflow is cancelled |
workflow.completed | A workflow reaches a terminal state |
workflow.timed_out | A workflow exceeded its timeoutMs and auto-transitioned |
Transitions
Section titled “Transitions”| Event type | When it fires |
|---|---|
transition.requested | A flowgate.command({workflowId, transition}) submit call is received |
transition.rejected | A submission is rejected (guard failure, version conflict, actor mismatch, etc.) |
transition.branched | A transition’s branch condition matched, changing the target state |
Deterministic chains
Section titled “Deterministic chains”| Event type | When it fires |
|---|---|
chain.step | One step in a deterministic chain completed |
chain.completed | A deterministic chain finished (reached a non-deterministic state, terminal, or depth limit) |
chain.failed | A deterministic chain stopped because an executor failed |
Executors
Section titled “Executors”| Event type | When it fires |
|---|---|
executor.started | An executor begins running |
executor.succeeded | An executor completed successfully |
executor.failed | An executor failed after all retries |
executor.retrying | An executor is retrying after a failure |
fallback.selected | The reliability layer is trying a fallback executor |
Guards
Section titled “Guards”| Event type | When it fires |
|---|---|
guard.evaluated | A guard was evaluated (payload includes pass/fail result) |
Example events
Section titled “Example events”workflow.started
Section titled “workflow.started”{ "id": "evt_a1b2c3d4e5f6789012345678abcdef01", "timestamp": "2025-06-15T14:32:08.123456Z", "workflow_id": "wf_d4e5f6a1b2c3", "correlation_id": "cor_f6a1b2c3d4e5789012345678abcdef01", "actor": "agent@default", "event_type": "workflow.started", "payload": { "definitionId": "deploy_pipeline", "input": { "service": "payments", "environment": "staging" } }}transition.requested
Section titled “transition.requested”{ "id": "evt_b2c3d4e5f6a1789012345678abcdef02", "timestamp": "2025-06-15T14:32:09.456789Z", "workflow_id": "wf_d4e5f6a1b2c3", "correlation_id": "cor_a1b2c3d4e5f6789012345678abcdef02", "actor": "agent@default", "event_type": "transition.requested", "payload": { "transition": "deploy", "expectedVersion": 4, "fromState": "ready_to_deploy" }}transition.rejected
Section titled “transition.rejected”{ "id": "evt_c3d4e5f6a1b2789012345678abcdef03", "timestamp": "2025-06-15T14:32:09.789012Z", "workflow_id": "wf_d4e5f6a1b2c3", "correlation_id": "cor_a1b2c3d4e5f6789012345678abcdef02", "actor": "agent@default", "event_type": "transition.rejected", "payload": { "transition": "deploy", "code": "GUARD_REJECTED", "message": "One or more guards rejected the transition." }}executor.succeeded
Section titled “executor.succeeded”{ "id": "evt_d4e5f6a1b2c3789012345678abcdef04", "timestamp": "2025-06-15T14:32:10.123456Z", "workflow_id": "wf_d4e5f6a1b2c3", "correlation_id": "cor_a1b2c3d4e5f6789012345678abcdef02", "actor": "agent@default", "event_type": "executor.succeeded", "payload": { "transition": "deploy", "candidate": 0, "attempt": 1, "kind": "cli" }}chain.completed
Section titled “chain.completed”{ "id": "evt_e5f6a1b2c3d4789012345678abcdef05", "timestamp": "2025-06-15T14:32:11.456789Z", "workflow_id": "wf_d4e5f6a1b2c3", "correlation_id": "cor_f6a1b2c3d4e5789012345678abcdef01", "actor": null, "event_type": "chain.completed", "payload": { "steps": 3, "finalState": "ready_to_deploy" }}Piping to your observability stack
Section titled “Piping to your observability stack”Audit events are structured JSON, one line per event. They’re designed to be piped into whatever you already use:
- Vector — use a
filesource pointing at your audit log, or capture stderr with astdinsource. - Prometheus — parse events into metrics (transitions per second, failure rate, latency histograms).
- Elasticsearch / OpenSearch — index the JSONL directly.
- CloudWatch / Datadog / Splunk — forward the log file or stderr stream through their agents.
The correlation_id field ties related events together across a single operation. The workflow_id groups everything for one workflow instance. Between the two, you can reconstruct the full story of any workflow run.
Filtering in dashboards
Section titled “Filtering in dashboards”When querying audit records, remember: tool_name and event_type are different axes.
Filter by tool (which MCP call triggered the record):
WHERE tool_name = 'flowgate.query' AND args->>'workflowId' IS NOT NULLThis finds all workflow-get and workflow-explain calls.
WHERE tool_name = 'flowgate.command' AND args->>'definitionId' IS NOT NULLThis finds all workflow-start calls.
Filter by what happened (event_type in the payload):
WHERE event_type = 'workflow.started'WHERE event_type = 'transition.requested' AND payload->>'transition' = 'deploy'The two axes are complementary. tool_name answers “what did the model call?” — event_type answers “what did the runtime record as a result?”