Runtime logs contract
Runtime logs are the on-disk diagnostics contract for every loom run --local invocation. The current implementation is phase-driven: event streams describe runtime boundaries with scope, phase_code, and phase_family, while summary and manifest documents give you stable pointers into the exact failing unit.
All paths below are relative to .loom/.runtime/logs/<run_id>/.
Contract versions
| Surface | Current version | Notes |
|---|---|---|
| Phase event streams and job or pipeline summaries or manifests | loom.runtime.logs.v2 | The main runtime contract for events.jsonl, pipeline/*.json, jobs/**/*.json |
Run-level summary.json | v1 | Convenience run summary with kind: "loom-run-local-logs" |
phase-report.json | v1 | Coverage and ordering report with kind: "loom-run-local-phase-report" |
Directory layout
<run_id>/
events.jsonl # run-scoped phase event stream
summary.json # run-level summary + phase_report_path
phase-report.json # phase coverage and validation report
pipeline/
summary.json # pipeline status + exit code
manifest.json # job roster + failing job pointers
events.jsonl # pipeline-scoped phase events
jobs/
<job_id>/
summary.json # job status + exit code
manifest.json # step roster + section roster + artifacts metadata
events.jsonl # job-scoped phase events
user/
execution/
events.jsonl # execution envelope + mirrored step events
script/
01/
summary.json # step 1 status + output_lines
events.jsonl # step 1 phase events + output
02/
summary.json
events.jsonl
system/
provider/
summary.json # provider_prepare section summary
events.jsonl
cache_restore/
summary.json
events.jsonl
artifact_extract/
summary.json
events.jsonl
cleanup/
summary.json
events.jsonl
artifacts/
<relative-path>...
artifacts.tar.gz
Step directories use 1-indexed, zero-padded labels (01, 02, ...) matching workflow script order.
There is no standalone section manifest file. Section roster entries live inside jobs/<job_id>/manifest.json and point to each section summary and event stream.
Phase model
The runtime emits a small set of stable phase identities. Optional phases appear only when that work actually exists for the job.
| Scope | Phase code | What it means |
|---|---|---|
run | run.bootstrap | Loom captured run context and isolated workspace metadata. |
run | run.pipeline_execute | Outer run envelope around pipeline execution. |
pipeline | pipeline.execute | The pipeline body from first runnable work to pipeline result. |
job | job.provider_prepare | Job start, provider setup, service startup, and similar pre-execution system work. |
job | job.cache_restore | Optional cache restore work before user execution. |
job | job.artifact_restore | The current artifact_extract section is normalized to this phase identity. |
job | job.execution | The job's user execution envelope. |
step | execution.script | A single script: step. Emitted per step and mirrored into broader streams. |
job | job.cache_save | Optional cache save work after execution. |
job | job.cleanup | Job cleanup boundary. A finish record is still emitted when the job never ran. |
run | run.finalize | Final contract-artifact writeout after pipeline completion. |
Phase family mapping
| Phase family | Phase codes |
|---|---|
orchestration | run.*, pipeline.execute, custom non-provider system phases |
provider | job.provider_prepare |
cache | job.cache_restore, job.cache_save |
artifact | job.artifact_restore |
user | job.execution, execution.* |
cleanup | job.cleanup |
The current local runtime artifact section is artifact_extract, and its summaries and event streams normalize to phase_code: "job.artifact_restore" with phase_family: "artifact". Build tooling around phase_code and phase_family, not the raw directory name alone.
Event stream contract
Every events.jsonl file is newline-delimited JSON. Each line is a single event record.
Guaranteed fields on every record
| Field | Type | Description |
|---|---|---|
schema_version | string | Always loom.runtime.logs.v2 |
ts | string | RFC 3339 timestamp with nanosecond precision |
seq | integer | Monotonic sequence number within that file |
level | string | Usually info, warn, or error |
event | string | phase_start, output, or phase_finish |
run_id | string | Run identifier |
pipeline_id | string | Pipeline identifier |
scope | string | run, pipeline, job, section, or step |
phase_code | string | Stable phase identity such as job.execution |
phase_family | string | Stable family such as user or cache |
Common contextual fields
| Field | Where it appears | Description |
|---|---|---|
job_name, job_id | Job, section, and step records | Job identity |
section_family, section | Section and step records | system or user family; section name such as provider or execution |
subphase, subphase_index | Step records | Step subphase identity, currently script and the 1-indexed step number |
step_index, step_id | Step records | Per-step identity |
stream, message | output records | Stream (stdout or stderr) and emitted message |
status, exit_code, duration_ms | phase_finish records | Outcome fields |
metrics | phase_finish records | Family-specific measurements and skip metadata |
skipped, skip_reason | Some system and cleanup finishes | Explicit skip markers |
isolated_workspace_path, isolated_workspace_source | Run bootstrap, provider prepare, cleanup | Workspace attribution fields |
Event kinds
| Event | Required extra fields | Purpose |
|---|---|---|
phase_start | Context fields for the scope | Marks the beginning of a phase boundary |
output | stream, message | Captured runtime output within the current phase |
phase_finish | status, duration_ms; often exit_code, metrics | Marks the end of a phase boundary |
Example:
{"schema_version":"loom.runtime.logs.v2","ts":"2026-03-07T12:00:03Z","seq":11,"level":"info","event":"phase_start","run_id":"loom-run-local-1772865600000000000","pipeline_id":"loom-local-1772865600000000000","scope":"job","phase_code":"job.execution","phase_family":"user","job_name":"build","job_id":"build"}
{"schema_version":"loom.runtime.logs.v2","ts":"2026-03-07T12:00:04Z","seq":12,"level":"info","event":"output","run_id":"loom-run-local-1772865600000000000","pipeline_id":"loom-local-1772865600000000000","scope":"step","phase_code":"execution.script","phase_family":"user","job_name":"build","job_id":"build","section_family":"user","section":"execution","subphase":"script","subphase_index":1,"step_index":1,"step_id":"script-01","stream":"stderr","message":"pnpm test"}
{"schema_version":"loom.runtime.logs.v2","ts":"2026-03-07T12:00:05Z","seq":13,"level":"error","event":"phase_finish","run_id":"loom-run-local-1772865600000000000","pipeline_id":"loom-local-1772865600000000000","scope":"step","phase_code":"execution.script","phase_family":"user","job_name":"build","job_id":"build","section_family":"user","section":"execution","subphase":"script","subphase_index":1,"step_index":1,"step_id":"script-01","status":"failed","exit_code":1,"duration_ms":942}
Summary and manifest documents
pipeline/summary.json
Always present. Current fields:
| Field | Type | Description |
|---|---|---|
schema_version | string | loom.runtime.logs.v2 |
run_id | string | Run identifier |
pipeline_id | string | Pipeline identifier |
status | string | success or failure |
exit_code | integer | 0 on success, non-zero on failure |
duration_ms | integer | Pipeline duration |
error | string | Present when the pipeline surfaces an error message |
pipeline/manifest.json
Always present. It is the top-level pointer document for job diagnostics.
| Field | Type | Description |
|---|---|---|
schema_version | string | loom.runtime.logs.v2 |
run_id, pipeline_id, status, exit_code, duration_ms | mixed | Pipeline identity and outcome. Current failure token is failure. |
jobs | object[] | Job roster entries |
failing_job_id | string | Present on failure |
failing_job_manifest_path | string | Present on failure |
Each jobs[] entry currently includes:
| Field | Type | Description |
|---|---|---|
job_name, job_id | string | Human-readable and filesystem-safe job identity |
status, exit_code, duration_ms | mixed | Job outcome |
job_manifest_path, job_summary_path | string | Relative pointers into the job contract artifacts |
system_events_path | string | Provider section events path for that job |
artifacts_path | string | Relative artifacts directory pointer |
artifacts_archive_path | string | Present when artifacts.tar.gz exists |
jobs/<job_id>/summary.json
Always present per job.
| Field | Type | Description |
|---|---|---|
schema_version | string | loom.runtime.logs.v2 |
run_id, pipeline_id, job_name, job_id | mixed | Job identity |
status, exit_code, duration_ms | mixed | Job outcome |
error | string | Present on failure when Loom has job-level error text |
jobs/<job_id>/manifest.json
Always present per job. This is the main branching document for user-step vs system-section failures.
| Field | Type | Description |
|---|---|---|
schema_version | string | loom.runtime.logs.v2 |
run_id, pipeline_id, job_name, job_id | mixed | Job identity |
status, exit_code, duration_ms | mixed | Job outcome |
user_steps | object[] | Step roster |
system_sections | object[] | Section roster with summary and events pointers |
artifacts | object | Artifact metadata object |
artifact_name | string | Present when the job declares a named artifact |
failing_section | string | Present on failure |
failing_step_index | integer | Present on user-step failure |
failing_step_events_path | string | Present on user-step failure |
Each user_steps[] entry currently includes:
| Field | Type | Description |
|---|---|---|
section | string | Currently script |
step_index, step_id | mixed | 1-indexed step identity |
command_preview | string | Truncated command preview |
step_summary_path, step_events_path | string | Relative pointers to the step artifacts |
Each system_sections[] entry currently includes:
| Field | Type | Description |
|---|---|---|
system_section | string | Raw section name such as provider or cache_restore |
section_family | string | system |
section | string | Same logical section name |
phase_code, phase_family | string | Normalized phase identity |
status, exit_code, duration_ms, output_lines | mixed | Section outcome and size |
metrics | object | Section metrics payload |
skipped, skip_reason | mixed | Present for explicit skipped sections |
summary_path, events_path | string | Relative pointers to the section artifacts |
Step summary
Path: jobs/<job_id>/user/execution/script/<NN>/summary.json
| Field | Type | Description |
|---|---|---|
schema_version | string | loom.runtime.logs.v2 |
run_id, pipeline_id, job_name, job_id | mixed | Step identity context |
section_family | string | user |
section | string | Currently script |
step_index, step_id | mixed | Step identity |
status, exit_code, duration_ms, output_lines | mixed | Step outcome and output count |
System section summary
Path: jobs/<job_id>/system/<section>/summary.json
| Field | Type | Description |
|---|---|---|
schema_version | string | loom.runtime.logs.v2 |
run_id, pipeline_id, job_name, job_id | mixed | Section identity context |
section_family | string | system |
section | string | Raw section name |
phase_code, phase_family | string | Normalized phase identity |
status, exit_code, duration_ms, output_lines | mixed | Section outcome and output count |
metrics | object | Section metrics payload |
skipped, skip_reason | mixed | Present for explicit skipped sections |
Skip and metrics semantics
Skipped work is represented as structured data, not just by omission.
| Surface | How skip appears | Current notes |
|---|---|---|
System section phase_finish and summary.json | status: "skipped" plus metrics.skipped: true; often top-level skip_reason and metrics.skip_reason too | Used for cache and artifact no-op or policy-driven skips |
Job manifest system_sections[] | Mirrors the same status, skipped, skip_reason, and metrics fields | Use this for pointer-first triage without opening the section summary first |
job.cleanup finish when a job never ran | status: "skipped" with skip metrics | This is the reliable skipped/not-run job boundary today |
| Step summaries | Unstarted steps resolve to status: "skipped" | No extra skip object is guaranteed today |
Current metric families:
| Phase family | Common metric keys |
|---|---|
cache | skipped, skip_reason, cache_name, cache_key, scope_hash, key_hash, hit |
artifact | skipped, skip_reason, file_count, archive_format, archive_path, archive_size_bytes |
user, cleanup, orchestration | Empty object or family-specific additions |
Unknown metric keys are allowed. Do not hardcode the full metric set.
Job artifacts
When Loom extracts job artifacts, it writes them under:
jobs/<job_id>/artifacts/
When an archive is produced, it is written at:
jobs/<job_id>/artifacts/artifacts.tar.gz
Artifact metadata
jobs/<job_id>/manifest.json currently includes an artifacts object for the job:
| Field | Type | Description |
|---|---|---|
base_path | string | Relative artifacts directory |
file_count | integer | Number of extracted files |
total_bytes | integer | Total extracted file bytes |
files | object[] | Per-file entries with path and size_bytes |
archive_path | string | Present when artifacts.tar.gz exists |
archive_format | string | Currently tar.gz when an archive exists |
archive_size_bytes | integer | Archive byte size when present |
The pipeline manifest mirrors top-level artifact pointers through artifacts_path and artifacts_archive_path.
phase-report.json
phase-report.json is a run-level validation artifact written beside the logs, not inside pipeline/ or a job directory.
| Field | Type | Description |
|---|---|---|
schema_version | string | v1 |
kind | string | loom-run-local-phase-report |
run_id | integer | Numeric run id used to build the log directory name |
events_jsonl_path | string | Absolute path to the run-scoped event stream |
started_at, finished_at, duration_ms | mixed | Run timing |
status, exit_code, error | mixed | Run outcome |
plan | object | Required phase instances Loom expected to see |
validation | object | Whether the observed timeline satisfied the contract |
coverage | object | Attributed runtime and required-phase coverage |
phase_metrics | object[] | Aggregated duration and completion counts by scope and phase code |
The report is useful when failure evidence is not enough and you need to answer questions like:
- Did a required phase boundary never emit?
- Did the current artifact extraction phase emit?
- How much runtime was attributed to cache, artifact, or execution phases?
Loom writes phase-report.json before surfacing validation failure, so the artifact remains available even when the phase timeline is invalid.
Pointer-first triage
Use this order for the fewest file reads:
| Step | File | What to look for |
|---|---|---|
| 1 | pipeline/summary.json | status, exit_code, and top-level pipeline error |
| 2 | pipeline/manifest.json | failing_job_id and failing_job_manifest_path |
| 3 | jobs/<job_id>/manifest.json | failing_step_events_path or the relevant system_sections[].events_path |
| 4 | The pointed events.jsonl | output lines and the closing phase_finish |
| 5 | phase-report.json | Optional: coverage, missing boundaries, and ordering issues |
Open one failing job first. Widen scope only after confirming the problem spans multiple jobs or a global run boundary.
Stability guidance
Safe to build against
- Pointer fields such as
failing_job_id,failing_job_manifest_path,failing_step_events_path,summary_path, andevents_path - Phase identity fields:
scope,phase_code,phase_family - Outcome fields on
phase_finish:status,duration_ms, and usuallyexit_code - The presence of
phase-report.json,pipeline/summary.json,pipeline/manifest.json, and per-jobsummary.jsonplusmanifest.json
Do not hardcode
- The full set of raw system section directory names
- Exact output message text
- The complete metric-key vocabulary inside
metrics - Whether every optional phase family appears for every job