Transaction Log Format
Forge records every command as machine-readable events in JSON Lines (JSONL) files. Use them for local reporting, cost tracking, and usage analytics, or send the same events to OpenSearch or an OTLP backend such as Progress Observability.
File locations
| Events | File |
|---|---|
| AI workflow commands and workflow engine events | .forge/work/<job_id>/logs/transaction.jsonl |
Utility commands without a job (for example frg config set-issue) | .forge/logs/utility.jsonl |
Each line is one JSON object. Forge appends events as they occur.
Execution identity fields
Every uploaded document carries two fields that identify the execution that emitted it:
| Field | Type | Description |
|---|---|---|
instance_id | string | Unique ID of the emitting execution, <type>-<uuid> (for example workflow-3f2a9c81-...) |
callers | array of strings | Ancestor instance IDs, root first. Omitted for a root execution; the last element is the direct parent and the length is the depth |
Note: These fields are not written to the local files. Forge adds them when it converts an event into an OpenSearch document or an OTLP span. In OTLP, they determine the trace and parent span, so a workflow and its commands and steps share one trace. Locally,
.forge/work/<job_id>/already scopes events to one job.
Use callers to reconstruct call trees. Do not use invocation_source: it is a manual or flow label kept for Progress Analytics.
Event types
event_type | Written when | File |
|---|---|---|
workflow | An AI workflow command starts or completes | transaction.jsonl |
utility | A utility command runs | utility.jsonl |
config_change | A command changes configuration or context | utility.jsonl |
validation | Input validation runs | transaction.jsonl |
output_tracking | Output validation compares expected and produced files | transaction.jsonl |
auto_retry | Auto-retry triggers | transaction.jsonl |
workflow_started, workflow_state_transition, branch_evaluated, step_execution, check_evaluation, approval | The workflow engine advances (see Workflow engine events) | transaction.jsonl |
Workflow event
{
"event_type": "workflow",
"command": "issue draft",
"start_time": "2026-04-20T23:34:22.753Z",
"start_commit_sha": "dd014a85",
"end_time": "2026-04-20T23:35:45.123Z",
"end_commit_sha": "98cec87b",
"trace_file": "traces/20260420_233418_issue.trace.md",
"success": true,
"model": "claude-sonnet-5",
"role": "product_manager",
"custom_agent": "none",
"token_usage": {
"input_tokens": 608900,
"cached_tokens": 556700,
"output_tokens": 5100
}
}
| Field | Type | Required | Description |
|---|---|---|---|
event_type | string | Yes | "workflow" |
command | string | Yes | Command executed, for example "issue draft" |
start_time | string | Yes | ISO 8601 start time |
start_commit_sha | string | No | Git commit at start; omitted outside a git repository |
end_time | string | No | ISO 8601 completion time |
end_commit_sha | string | No | Git commit at completion |
trace_file | string | No | Trace file path, relative to the job directory |
success | boolean | No | Whether the command succeeded |
error_message | string | No | Error message when the command failed |
token_usage | object | No | Token counts (see below) |
model | string | Yes | AI model. "not set" for start events or when unavailable |
role | string | Yes | Role prompt. "none" when a custom agent is used; "not set" for start events |
custom_agent | string | Yes | Custom agent name. "none" when not configured; "not set" for start events |
agent | string | No | AI agent that ran the command |
workflow_type, role_prompt_type, task_prompt_type | string | No | Provenance of the workflow definition and composed prompts; see Provenance fields. |
model, role, and custom_agent are always present:
"not set"means the value was not available yet, for example in a start event."none"means intentional absence, for example no custom agent.- In completion events,
roleandcustom_agentare mutually exclusive: whencustom_agenthas a value,roleis"none".
Exclude these sentinel values when you aggregate.
Provenance fields
The provenance fields identify the sources used for a workflow command:
| Field | Values | Meaning |
|---|---|---|
workflow_type | built-in, modified, custom | Whether the executed definition is built in, a built-in definition with prompt overrides, or user-defined. |
role_prompt_type | built-in, custom, disabled | Source of the composed role prompt. disabled means no role prompt was used, such as with a custom agent or --role none. |
task_prompt_type | built-in, custom | Source of the composed task prompt. |
workflow_type is omitted when the execution does not resolve a single catalog command. role_prompt_type is omitted when Forge did not compose a role prompt; when a prompt is composed without a role, it is explicitly disabled. task_prompt_type is omitted when no task prompt was composed. Auto-retry continuations retain workflow_type but omit prompt provenance because the retry prompt is generated at runtime. Missing keys are not serialized as null or a default, and readers should tolerate them, including in older logs.
Token usage
token_usage is present when the agent reports token counts. Claude Code CLI, GitHub Copilot CLI, and OpenCode CLI report token usage.
| Field | Type | Description |
|---|---|---|
input_tokens | integer | Number of tokens sent to the model (prompt size) |
cached_tokens | integer | Number of tokens served from cache (cache reads, reduces cost) |
cache_write_tokens | integer | Number of tokens written to cache this turn (cache creation, priced separately from reads; omitted when the agent doesn't report it) |
output_tokens | integer | Number of tokens generated by the model (completion size) |
reasoning_tokens | integer | Number of reasoning tokens used (optional, when available) |
Counts are individual tokens. To estimate billable input, subtract cached tokens: 608900 - 556700 = 52200.
Utility and config change events
Utility commands write to .forge/logs/utility.jsonl. This output is from frg config set-issue 7:
{"event_type":"config_change","field":"issue_id","timestamp":"2026-09-14T19:09:33.975069Z","old_value":null,"new_value":"7"}
{"event_type":"utility","command":"config set-issue","timestamp":"2026-09-14T19:09:33.981651Z","parameters":{"issue_id":"7"},"success":true}
utility events are also forwarded to Progress Analytics when usage telemetry is enabled.
Workflow engine events
frg workflow run writes six event types as the state machine advances. They reach a configured OpenSearch or OTLP backend, but not Progress Analytics, which accepts only workflow and utility events.
event_type | Written when | Key fields |
|---|---|---|
workflow_started | A workflow execution begins | workflow_type, workflow_id, execution_id, initial_state |
workflow_state_transition | The state machine changes state | from_state, to_state, outcome, execution_id, command |
branch_evaluated | A branch condition is evaluated | from_state, to_state, condition_desc, result |
step_execution | A shell, builtin, or agent step finishes | step_id, step_type, phase, outcome, duration_ms |
check_evaluation | A check finishes | check_id, check_type, phase, result, action |
approval | An approval gate is released or bypassed | gate_id, step, approver_email, approval_code, method |
{
"event_type": "approval",
"timestamp": "2026-04-20T23:38:02.000Z",
"workflow_id": "392",
"workflow_type": "issue-to-pr",
"execution_id": "20260420_233418",
"gate_id": "code_review_gate",
"step": "code_review",
"approver_email": "dev@example.com",
"approval_code": "LGTM",
"method": "manual",
"success": true
}
Warning:
approvalevents are the only events that carry approver identity. OpenSearch receivesapprover_email(hashed when[privacy] strict_privacy = true) andapproval_code(always verbatim). OTLP spans omit both fields, and Progress Analytics never receives approval events. Do not use secrets as approval codes. See Security Guide.
Query examples
Run these jq commands against a transaction.jsonl file.
Total tokens consumed:
jq -s 'map(select(.token_usage != null))
| map(.token_usage.input_tokens + .token_usage.output_tokens) | add' transaction.jsonl
Success and failure counts:
jq -s 'map(select(.event_type == "workflow" and .success != null))
| group_by(.success) | map({success: .[0].success, count: length})' transaction.jsonl
Average duration in seconds (strips fractional seconds, which fromdateiso8601 does not accept):
jq -s 'map(select(.event_type == "workflow" and .end_time != null))
| map((.end_time | sub("\\.[0-9]+"; "") | fromdateiso8601)
- (.start_time | sub("\\.[0-9]+"; "") | fromdateiso8601))
| add / length' transaction.jsonl
Token usage by model:
jq -s 'map(select(.model != null and .model != "not set" and .token_usage != null))
| group_by(.model)
| map({model: .[0].model,
total_input: map(.token_usage.input_tokens) | add,
total_output: map(.token_usage.output_tokens) | add})' transaction.jsonl
Custom agent usage:
jq -s 'map(select(.custom_agent != null and .custom_agent != "none" and .custom_agent != "not set"))
| group_by(.custom_agent) | map({agent: .[0].custom_agent, count: length})
| sort_by(.count) | reverse' transaction.jsonl
To analyze several jobs, pass multiple files, for example .forge/work/*/logs/transaction.jsonl. For large files, process events as a stream (jq -c 'select(.event_type == "workflow")') instead of slurping with -s.
Backward compatibility
- New fields are added without changing existing fields. Parsers should ignore unknown fields and check for optional fields before reading them.