New to FORGE? Start a free 30-day trial

Transaction Log Format

Updated on Oct 5, 2026

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

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

FieldTypeDescription
instance_idstringUnique ID of the emitting execution, <type>-<uuid> (for example workflow-3f2a9c81-...)
callersarray of stringsAncestor 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_typeWritten whenFile
workflowAn AI workflow command starts or completestransaction.jsonl
utilityA utility command runsutility.jsonl
config_changeA command changes configuration or contextutility.jsonl
validationInput validation runstransaction.jsonl
output_trackingOutput validation compares expected and produced filestransaction.jsonl
auto_retryAuto-retry triggerstransaction.jsonl
workflow_started, workflow_state_transition, branch_evaluated, step_execution, check_evaluation, approvalThe workflow engine advances (see Workflow engine events)transaction.jsonl

Workflow event

json
{
 "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
 }
}
FieldTypeRequiredDescription
event_typestringYes"workflow"
commandstringYesCommand executed, for example "issue draft"
start_timestringYesISO 8601 start time
start_commit_shastringNoGit commit at start; omitted outside a git repository
end_timestringNoISO 8601 completion time
end_commit_shastringNoGit commit at completion
trace_filestringNoTrace file path, relative to the job directory
successbooleanNoWhether the command succeeded
error_messagestringNoError message when the command failed
token_usageobjectNoToken counts (see below)
modelstringYesAI model. "not set" for start events or when unavailable
rolestringYesRole prompt. "none" when a custom agent is used; "not set" for start events
custom_agentstringYesCustom agent name. "none" when not configured; "not set" for start events
agentstringNoAI agent that ran the command
workflow_type, role_prompt_type, task_prompt_typestringNoProvenance 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, role and custom_agent are mutually exclusive: when custom_agent has a value, role is "none".

Exclude these sentinel values when you aggregate.

Provenance fields

The provenance fields identify the sources used for a workflow command:

FieldValuesMeaning
workflow_typebuilt-in, modified, customWhether the executed definition is built in, a built-in definition with prompt overrides, or user-defined.
role_prompt_typebuilt-in, custom, disabledSource of the composed role prompt. disabled means no role prompt was used, such as with a custom agent or --role none.
task_prompt_typebuilt-in, customSource 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.

FieldTypeDescription
input_tokensintegerNumber of tokens sent to the model (prompt size)
cached_tokensintegerNumber of tokens served from cache (cache reads, reduces cost)
cache_write_tokensintegerNumber of tokens written to cache this turn (cache creation, priced separately from reads; omitted when the agent doesn't report it)
output_tokensintegerNumber of tokens generated by the model (completion size)
reasoning_tokensintegerNumber 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:

jsonl
{"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_typeWritten whenKey fields
workflow_startedA workflow execution beginsworkflow_type, workflow_id, execution_id, initial_state
workflow_state_transitionThe state machine changes statefrom_state, to_state, outcome, execution_id, command
branch_evaluatedA branch condition is evaluatedfrom_state, to_state, condition_desc, result
step_executionA shell, builtin, or agent step finishesstep_id, step_type, phase, outcome, duration_ms
check_evaluationA check finishescheck_id, check_type, phase, result, action
approvalAn approval gate is released or bypassedgate_id, step, approver_email, approval_code, method
json
{
 "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: approval events are the only events that carry approver identity. OpenSearch receives approver_email (hashed when [privacy] strict_privacy = true) and approval_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:

bash
jq -s 'map(select(.token_usage != null))
 | map(.token_usage.input_tokens + .token_usage.output_tokens) | add' transaction.jsonl

Success and failure counts:

bash
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):

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

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

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

See Also