Skip to main content

Artifacts & Run Tracking

Agent Actions generates artifacts for debugging, auditing, and resuming interrupted runs. Understanding this structure helps you inspect what happened during execution and diagnose issues.

Complete Directory Structure​

text
TXT
project/
├── .agac/
│ └── batch_state/ # What an offline-provider batch was submitted with,
│ # so the run that collects it can find it. Reclaimed
│ # when the batch registry stops naming the batch.
├── artefact/
│ ├── catalog.json # Project catalog (agac docs)
│ ├── runs.json # Workflow execution history (agac run + agac docs)
│ └── rendered_workflows/ # Partial rendered configs saved when preflight render fails
├── logs/
│ └── agent_actions.log # Application logs
└── agent_workflow/
└── my_workflow/
└── agent_io/
├── .agent_status.json # Per-action execution state
├── staging/ # Input data
├── store/
│ └── {workflow_name}.db # SQLite storage backend
├── logs/
│ ├── .manifest.json # Workflow execution manifest
│ ├── run_results.json # Summary metrics and timing
│ ├── events.json # Full event telemetry (JSON Lines)
│ └── errors.json # Error-level events only (JSON Lines)
└── target/
└── {action_name}/ # Per-action output directories

Runtime Artifacts​

Workflow Manifest (.manifest.json)​

Path: agent_io/logs/.manifest.json

The manifest tracks the execution plan and status for the entire workflow run. Created when the workflow starts, updated as actions complete.

json
JSON
{
"schema_version": "1.0",
"workflow_name": "product_pipeline",
"workflow_run_id": "run_abc123",
"pid": 48213,
"host_id": "9f2c1a77b4e03d58",
"started_at": "2026-03-24T10:00:00Z",
"completed_at": "2026-03-24T10:02:30Z",
"status": "completed",
"execution_order": ["extract_data", "generate_content"],
"levels": [[0, "extract_data"], [1, "generate_content"]],
"actions": {
"extract_data": {
"status": "completed",
"output_dir": "extract_data",
"dependencies": [],
"record_count": 5,
"started_at": "2026-03-24T10:00:01Z",
"completed_at": "2026-03-24T10:01:15Z"
}
}
}

pid and host_id identify the process that owns the run. A process killed outright never records a terminal status, so readers use them to check whether that process still exists and report its in-flight actions as interrupted rather than showing them as running forever. host_id is a hash of the hostname, not the hostname, because this file is published by agac docs. A reader that finds no pid, or a host_id that is not its own, makes no judgement at all.

The VS Code Workflow Navigator reads this file to display the sidebar tree view and DAG visualization.

Action Status (.agent_status.json)​

Path: agent_io/.agent_status.json

Persists per-action execution state for resumable runs:

json
JSON
{
"extract_data": {"status": "completed", "record_limit": 2, "file_limit": null,
"records_processed": 2, "truncated": true},
"generate_content": {"status": "completed", "record_limit": null, "file_limit": null,
"records_processed": 40, "truncated": false},
"validate_output": {"status": "pending"}
}

A completed action also stores what it ran under, so the next run can tell an action that is genuinely finished from one that was cut short. record_limit and file_limit are the limits that were in force — from the workflow config, the flag, or the environment variable, whichever won — not the ones the config asks for. Alongside them are records_processed and truncated, which say what the run actually did rather than what it was asked for, and model_name, model_vendor and a config_hash over the prompt, schema and guard. The model is deliberately outside that hash — it is compared from model_name/model_vendor instead, so folding it in could not rotate every stored digest at once.

A changed record_limit re-runs the action only when it could have held something back. An untruncated run processed everything there was, so a limit at or above that count would drop nothing either and the action stays complete; a run that did truncate never vouches for any limit, and a stamp that cannot say — written before these counts existed, by a run that never sliced, or by one that lost an input file to an error, since records it never reached were never counted — keeps the coarse rule and re-runs. file_limit has no count to reason from and stays coarse throughout: any change to it re-runs the action, including a change to a value larger than the number of files. If anything else in the stamp differs on a later run, the action is reset to pending and re-executed rather than skipped.

An action also records, when it starts work, what that work is answered under: answered_under, holding the same config_hash, model_name and model_vendor. A run that stops partway through an action — killed, interrupted, or stopped by an error — leaves the records it finished, and the next run keeps them only while these still match the config. If the prompt, schema, guard or model was edited in between, the action and every action that reads it answer everything again, and a batch still out under the old config is given up. Only those four count as edits: a change to a tool's code, to seed data or to the context scope is not seen, so run agac run --fresh to answer everything again after one. A guard's verdict is not kept: the run that resumes the action puts each record to the guard again, so a record its code now skips is skipped, and one it now passes is answered. The exception is a record of a batch action whose batch was out, or finished and not yet collected, when the run stopped: it keeps the answer that batch brings, since the guard judged it when the batch was sent and collecting it judges nothing. A record finished in a file the stopped run had not written yet is answered again when an earlier run had stored that file, since the stored row predates its answer. A file the store fails to write, on a full disk or a database error, stops the action this way: the run ends failed, and the next one writes that file. Online, the run writes the action's other files first, and after an edit each run that fails to store the file asks for its records again, as above. A batch collect pass stops at the file, and a later run collects it and the files after it without sending any of them again. An agac retry records what the records it does not name were answered under instead — the stamp of the action's last completion, or what its last run recorded if it never completed — so a retry, stopped or finished, leaves an edit to the next run.

A reset leaves what the action stored in place, and the re-run replaces it file by file. A file in which every record of the re-run fails — the model's answers do not parse, or a tool of record granularity raises on each or answers each with output its schema refuses, or a tool of file granularity raises on the file — is stored with those failures, as a file in which only some fail is, so neither the action nor what reads it keeps a row answered before the edit, and agac retry asks for them again. Outside a reset, a run that answers nothing in a file and fails some of it leaves what that file held.

Staged input is the source of truth for what an action holds. An action reset after its input was removed — by an edit, or by a limit or model given on the command line — finds no input file, is skipped, and its stored output is deleted along with its readers'. Restore the input and run again to rebuild them. A reset after only some of its input files were removed deletes what it stored for those files, once it has read every file still there, and so do its readers; a batch action does this when it submits. A file that failed to process keeps its output. Nothing is deleted by a run that a file limit stopped before its last file, by one that could not list part of its input or read what its upstream stored, or by an agac retry repairing the records it names. A retry that names none, as one resuming a halted action does, runs its actions in full and deletes as a run does. A version merge sees every file its versions hold whatever its file limit, so before it runs it deletes what it stored for a file none of them holds, under agac retry too. Restore the files and run with --fresh to rebuild them. A file every version holds empty leaves the merge, and what reads it, holding nothing for that file. What a merge stores is only its answers: agac retry, or a run resumed after it stopped, keeps them for each record it does not ask again.

StatusDescription
pendingNot yet executed
runningCurrently executing
completedSuccessfully finished
failedTerminated with error
interruptedThe run was killed while this action was executing, or the process that owned it no longer exists
skippedSkipped by guard; because a dependency (or version source) failed or was skipped; because every version source came back empty; or because the action found no input file. A skipped action whose input holds nothing holds nothing either: the rows it stored before are deleted. One whose input still holds rows (a failed run keeps its answers) keeps its own
batch_submittedBatch job submitted, awaiting results

Re-running a workflow skips completed actions and resumes from the failure point, keeping what an action stopped partway had finished unless it was edited since.

Run Results (run_results.json)​

Path: agent_io/logs/run_results.json

Summary of the workflow execution with per-action metrics:

json
JSON
{
"metadata": {
"invocation_id": "inv_abc123",
"workflow_name": "product_pipeline",
"action_count": 3,
"execution_mode": "parallel"
},
"results": [
{
"action_name": "extract_data",
"status": "completed",
"execution_time": 4.2,
"record_count": 10,
"tokens": 1500,
"output_folder": "extract_data"
}
],
"elapsed_time": 12.5,
"total_tokens": 4500
}

Run History (runs.json)​

Path: artefact/runs.json

runs.json is the cumulative catalog of past workflow executions surfaced by the documentation site. Every agac run writes to it twice via RunTracker:

  • At start — a new execution entry is appended with status: "running", started_at, and the action plan.
  • At end — the entry is updated with the final status (success / failed / paused), ended_at, duration_seconds, and any error_message. Aggregate workflow metrics (success_rate, avg_duration_seconds) are recomputed in the same write.

agac docs reads runs.json to render the Run History view in the documentation site; if the file is missing when agac docs runs, an empty catalog is initialized so the page renders even before the first run completes.

json
JSON
{
"metadata": {"generated_at": "2026-03-24T10:00:00Z", "total_runs": 12},
"executions": [
{
"id": "run_product_pipeline_a1b2c3d4",
"workflow_id": "product_pipeline",
"workflow_name": "product_pipeline",
"status": "success",
"started_at": "2026-03-24T10:00:00Z",
"ended_at": "2026-03-24T10:02:30Z",
"duration_seconds": 150.0,
"actions_completed": 5,
"actions_total": 5
}
],
"workflow_metrics": {
"product_pipeline": {
"total_runs": 8,
"successful_runs": 7,
"failed_runs": 1,
"success_rate": 0.875,
"avg_duration_seconds": 142.3
}
}
}

The most recent 100 executions are kept; older entries roll off as new runs land. The authoritative per-run summary is still agent_io/logs/run_results.json — that file captures the full per-action breakdown for the single run that produced it.

Events Log (events.json)​

Path: agent_io/logs/events.json

Complete telemetry of all system events in JSON Lines format (one event per line):

jsonl
JSONL
{"event_type": "WorkflowStartEvent", "code": "W001", "level": "info", "category": "workflow", "message": "Running workflow product_pipeline (4 actions)", "diagnostic": false, "meta": {"timestamp": "2026-03-24T10:00:00Z", "correlation_id": null, "invocation_id": null, "thread_id": null}, "data": {"workflow_name": "product_pipeline", "action_count": 4, "execution_mode": "parallel"}}
{"event_type": "StepStartEvent", "code": "W004", "level": "info", "category": "workflow", "message": "Step 2/3: extract_data", "diagnostic": false, "meta": {"timestamp": "2026-03-24T10:00:01Z", "correlation_id": null, "invocation_id": null, "thread_id": null}, "data": {"step_index": 1, "total_steps": 3, "actions": ["extract_data"], "pending": ["extract_data"]}}
{"event_type": "ActionStartEvent", "code": "A001", "level": "info", "category": "action", "message": "2/4 START extract_data", "diagnostic": false, "meta": {"timestamp": "2026-03-24T10:00:01Z", "correlation_id": null, "invocation_id": null, "thread_id": null}, "data": {"action_name": "extract_data", "action_index": 1, "total_actions": 4, "action_type": "llm", "input_path": "", "mode": "online"}}
{"event_type": "LLMRequestEvent", "code": "L001", "level": "debug", "category": "llm", "message": "LLM request to openai/gpt-4o (412 prompt tokens)", "diagnostic": false, "meta": {"timestamp": "2026-03-24T10:00:02Z", "correlation_id": null, "invocation_id": null, "thread_id": null}, "data": {"provider": "openai", "model": "gpt-4o", "action_name": "extract_data", "prompt_tokens": 412, "request_id": ""}}
{"event_type": "LLMResponseEvent", "code": "L002", "level": "debug", "category": "llm", "message": "LLM response: 500 tokens in 1840ms", "diagnostic": false, "meta": {"timestamp": "2026-03-24T10:00:03Z", "correlation_id": null, "invocation_id": null, "thread_id": null}, "data": {"provider": "openai", "model": "gpt-4o", "action_name": "extract_data", "prompt_tokens": 412, "completion_tokens": 88, "total_tokens": 500, "latency_ms": 1840.0, "request_id": ""}}
{"event_type": "ActionCompleteEvent", "code": "A002", "level": "info", "category": "action", "message": "2/4 OK extract_data in 2.10s", "diagnostic": false, "meta": {"timestamp": "2026-03-24T10:00:04Z", "correlation_id": null, "invocation_id": null, "thread_id": null}, "data": {"action_name": "extract_data", "action_index": 1, "total_actions": 4, "execution_time": 2.1, "output_path": "", "record_count": 23, "tokens": {}, "mode": "online", "model_vendor": "openai", "model_name": "gpt-4o", "kind": "llm"}}
{"event_type": "StepCompleteEvent", "code": "W005", "level": "info", "category": "workflow", "message": "Step 2/3 complete in 2.10s", "diagnostic": false, "meta": {"timestamp": "2026-03-24T10:00:04Z", "correlation_id": null, "invocation_id": null, "thread_id": null}, "data": {"step_index": 1, "total_steps": 3, "elapsed_time": 2.1, "completed": 1, "partial": 0, "skipped": 0, "failed": 0, "unfinished": 0, "batch_pending": []}}

Every line carries the same envelope. A parser should key on event_type or code; the payload is always under data, and the timestamp under meta, never at the top level.

KeyWhat it holds
event_typeThe class name, e.g. ActionStartEvent
codeStable short code, e.g. A001 — prefer this over the class name for matching
leveldebug, info, warn or error
categoryCoarse grouping, e.g. action, llm, data_processing
messageRendered human-readable line
diagnostictrue when the line describes framework internals. The console hides these unless the run is verbose; they are always written here
metatimestamp, correlation_id, invocation_id, thread_id
dataPer-event fields; the shape differs by event_type

Event Codes​

Codes are prefix + ordinal. The prefix groups related events; it is not always the same string as category.

PrefixCategoryExamples
WworkflowWorkflowStartEvent (W001), WorkflowCompleteEvent (W002), WorkflowFailedEvent (W003), StepStartEvent (W004), StepCompleteEvent (W005)
AactionActionStartEvent (A001), ActionCompleteEvent (A002), ActionSkipEvent (A003), ActionFailedEvent (A004), ActionCachedEvent (A005)
LllmLLMRequestEvent (L001), LLMResponseEvent (L002), LLMErrorEvent (L003), RateLimitEvent (L004), LLMJSONParseErrorEvent (L005)
TtemplateTemplateRenderingFailedEvent (T001)
BbatchBatchSubmittedEvent (B001), BatchProgressEvent (B002), BatchCompleteEvent (B003), BatchStatusEvent (B008), BatchSubmissionFailedEvent (B009)
BPdata_processingBatchProcessingStartedEvent (BP001), BatchProcessingProgressEvent (BP002), BatchDataProcessingCompleteEvent (BP003)
RPdata_processingRecordProcessingStartedEvent (RP001), RecordFilteredEvent (RP002), RecordTransformedEvent (RP003), RecordEmptyOutputEvent (RP005)
DTdata_processingEnrichmentPipelineStartedEvent (DT001), EnricherExecutedEvent (DT002), EnrichmentPipelineCompleteEvent (DT003)
RCdata_processingResultCollectionStartedEvent (RC001), ResultCollectedEvent (RC002), ResultCollectionCompleteEvent (RC003), ExhaustedRecordEvent (RC004)
CXdataContextNamespaceLoadedEvent (CX001), ContextFieldSkippedEvent (CX002), ContextScopeAppliedEvent (CX003), ContextFieldNotFoundEvent (CX006)
DdataDataParsingErrorEvent (D001), DataLoadingErrorEvent (D002)
VvalidationValidationStartEvent (V001), ValidationCompleteEvent (V002), ValidationErrorEvent (V003), ValidationWarningEvent (V004)
DVvalidationDataValidationStartedEvent (DV001), DataValidationPassedEvent (DV002), DataValidationFailedEvent (DV003)
GguardGuardEvaluationTimeoutEvent (G001), GuardEvaluationErrorEvent (G002)
RrecoveryRetryExhaustedEvent (R001)
SOschemaSchemaConstructionStartedEvent (SO001), SchemaConstructionCompleteEvent (SO002)
FIOfile_ioSourceDataSavingEvent (FIO001), SchemaLoadedEvent (FIO004), FileWriteStartedEvent (FIO005), FileWriteCompleteEvent (FIO006), FileWriteFailedEvent (FIO007)
CcacheCacheHitEvent (C001), CacheMissEvent (C002), CacheInvalidationEvent (C003)
FconfigurationConfigLoadStartEvent (F001), ConfigLoadEvent (F002)
IinitializationCLIInitStartEvent (I001), WorkflowInitializationStartEvent (I008), ProjectInitializedEvent (I013)
PpluginUDFDiscoveryStartEvent (P001), UDFDiscoveryCompleteEvent (P003)

Recovery Events​

  • R001 RetryExhaustedEvent — fired when transport retry gives up. Contains the operation, the attempt count and the last error.
  • RC004 ExhaustedRecordEvent — fired when a record is exhausted after recovery failed. Contains action_name, record_index, source_guid and reason.

A run whose records all succeed first time produces neither. This is by design — the events only fire when recovery actually occurs.

File Lifecycle​

Both events.json and errors.json accumulate across runs by default. When --fresh is passed, both files are deleted before the run starts and recreated on the first event write. File watchers must handle the file being absent between deletion and first event.

Errors Log (errors.json)​

Path: agent_io/logs/errors.json

ERROR-level events only — a filtered subset of events.json for quick error diagnosis:

jsonl
JSONL
{"type": "ValidationFailEvent", "action": "extract_data", "error": "Required field 'name' missing"}
{"type": "ActionFailedEvent", "action": "generate_content", "error": "Rate limit exceeded"}

:::caution Monitoring change LLMJSONParseErrorEvent (code L005) is now WARN level — it no longer appears in errors.json. Parse errors the repair loop recovers from are not true errors. Consumers monitoring parse failures must switch to events.json filtered by code L005. :::

tip

When debugging, check errors.json first for a quick overview, then dive into events.json for the full trace around the failure timestamp.

Storage Backend (SQLite)​

Path: agent_io/store/{workflow_name}.db (one database per workflow)

The SQLite database stores structured workflow data:

TablePurpose
source_dataInput records with deduplication by source_guid
target_dataAction outputs organized by action_name
record_dispositionTracks the fate of individual records

Record Dispositions​

DispositionMeaning
successRecord processed successfully
passthroughAnswered with nothing, kept as a tombstone (on_empty: skip)
skippedLeft out of this action before its guard ran, or the whole action was skipped
filteredRemoved from this action and every action below it (guard with on_false: filter)
exhaustedRecovery gave up — retry attempts or expect iterations spent
failedProcessing failed
unprocessedNot answered: an upstream failure cascaded, or a guard with on_false: skip skipped it. The next run puts it to the guard again

The failed and exhausted dispositions also store an input_snapshot column containing the JSON-serialized input record at the time of failure (truncated to 10KB). This enables post-mortem debugging even after batch recovery files have been cleaned up. Other disposition types do not populate this column.

Querying the Database​

bash
BASH
sqlite3 agent_io/store/my_workflow.db

-- List all actions with output
SELECT DISTINCT action_name FROM target_data;

-- Count records per action
SELECT action_name, SUM(record_count) FROM target_data GROUP BY action_name;

-- Preview data from an action
SELECT data FROM target_data WHERE action_name = 'extract_data' LIMIT 1;

-- Check record dispositions
SELECT action_name, disposition, COUNT(*) FROM record_disposition GROUP BY action_name, disposition;

Or use the CLI preview command:

bash
BASH
agac preview -w my_workflow
agac preview -w my_workflow -a extract_data -f json
agac preview -w my_workflow --stats

Output Record Structure​

Each output record contains tracking metadata:

json
JSON
{
"source_guid": "37812c37-80a2-596b-8747-8f93e7a34e7f",
"content": {
"product_name": "Smart Fitness Tracker",
"category": "Wearable Technology"
},
"target_id": "dd45e6ef-63d6-45f6-8759-5df33c9a84a2",
"node_id": "node_0_86a1e066-9522-4e0e-a3db-e83c7e38ab8c",
"lineage": ["node_0_86a1e066-9522-4e0e-a3db-e83c7e38ab8c"]
}
FieldDescription
source_guidLinks to original input record
contentSchema-validated action output
target_idUnique identifier for this output
node_idAction execution identifier
lineageChain of node_ids for provenance tracking

Documentation Catalog​

Path: artefact/catalog.json (generated by agac docs)

The catalog is a snapshot of your entire project for the documentation site:

bash
BASH
# Build and serve the documentation site
agac docs

The catalog contains: workflow definitions, action metadata, prompt templates, schema definitions, run history, execution metrics, and a bounded tail of the event stream shared across every log the project wrote. See Documentation Site for the full browsable interface.

Logs​

bash
BASH
# Set log level via environment variable
AGENT_ACTIONS_LOG_LEVEL=DEBUG agac run -a my_workflow

# Or use the --debug flag, which goes before the command
agac --debug run -a my_workflow

Log file location: {project_root}/logs/agent_actions.log

Useful Commands​

bash
BASH
# Inspect run results
cat agent_io/logs/run_results.json | python3 -m json.tool

# Check for errors
cat agent_io/logs/errors.json

# Count events by type
cat agent_io/logs/events.json | python3 -c "
import sys, json, collections
counts = collections.Counter()
for line in sys.stdin:
counts[json.loads(line)['type']] += 1
for k, v in counts.most_common():
print(f'{v:4d} {k}')
"

# Preview action output via CLI
agac preview -w my_workflow -a extract_data

# Check workflow status
agac status -a my_workflow

See Also​

  • Data I/O — Input/output directory structure and storage backends
  • Data Lineage — Tracking records across multi-action workflows
  • Documentation Site — Interactive project documentation
  • Logging — Event system and log configuration