Husky output passing guide
Output passing lets one job capture structured values from its own execution and feed those values into downstream jobs in the same pipeline cycle.
This is one of Husky's most important pipeline features because it removes the need for ad hoc temp files and hard-coded shared paths.
Core idea
An upstream job declares output: capture rules.
A downstream job references those values with:
{{ outputs.<job_name>.<var_name> }}
Husky stores captured values in run_outputs and scopes them to a cycle_id, so outputs from one execution chain do not leak into another.
Example
jobs:
ingest:
description: "Ingest source data and print the generated file path"
frequency: daily
time: "0200"
command: "./scripts/ingest.sh"
output:
file_path: last_line
transform:
description: "Transform the file produced by ingest"
frequency: after:ingest
command: "python transform.py --input {{ outputs.ingest.file_path }}"
If ingest prints /tmp/data-2026-03-09.json on its final line, Husky stores that as outputs.ingest.file_path for the current cycle and injects it into transform at dispatch time.
Supported capture modes
last_line
Captures the final non-empty stdout line.
output:
file_path: last_line
Good for scripts that print a single final artifact path or ID.
first_line
Captures the first stdout line.
output:
build_id: first_line
json_field:<key>
Parses stdout as JSON and extracts a top-level key.
output:
count: json_field:total
Example stdout:
{"total":42,"ok":true}
Captured value becomes 42.
regex:<pattern>
Extracts the first regex capture group from stdout.
output:
version: regex:v([0-9.]+)
Example stdout line:
published version v2.4.1
Captured value becomes 2.4.1.
exit_code
Captures the numeric exit code.
output:
code: exit_code
Useful for downstream reporting or conditional logic outside Husky.
Where templates can be used
Output templates are rendered at dispatch time in:
command- job
envvalues
Examples:
command: "node report.js --input {{ outputs.ingest.file_path }}"
env:
INPUT_FILE: "{{ outputs.ingest.file_path }}"
RECORD_COUNT: "{{ outputs.transform.count }}"
cycle_id scoping
Every root trigger chain gets a generated cycle_id.
A root trigger chain can start from:
- a scheduled tick
- a manual run
- a recovery-triggered retry/catchup sequence
That cycle_id propagates through downstream dependency execution.
Why it matters:
- outputs from cycle A are not visible to cycle B
- independent manual runs do not cross-contaminate pipeline data
- debugging remains deterministic even for repeated runs of the same jobs
What happens when data is missing
If a job references {{ outputs.job.var }} but there is no matching value for the current cycle_id, dispatch fails with a descriptive error.
That failure happens at dispatch time rather than silently rendering an empty string.
Design constraints and best practices
Prefer structured stdout for structured data
If a script already knows how to emit JSON, use json_field:<key> instead of regex parsing.
Keep captured values small
Outputs are stored in SQLite and intended for identifiers, artifact paths, counts, and similar small payloads. Avoid dumping large documents or binary-like blobs to stdout just to capture them.
Make captured values intentional
Design job scripts so the captured line or JSON field is stable and documented.
Avoid ambiguous stdout
If a job writes lots of human-readable logs, last_line can be fragile unless the script intentionally prints the final machine-readable output last.
Example patterns
Pass a generated file path
output:
file_path: last_line
Pass a JSON result count
output:
count: json_field:total
Capture a release version from tool output
output:
version: regex:release\s+([0-9.]+)
Use outputs in env
env:
ARTIFACT_PATH: "{{ outputs.build.file_path }}"
Storage model
Captured outputs are stored in the run_outputs table with:
run_idjob_namevar_namevaluecycle_id
This allows:
- per-run inspection
- cycle-level debugging
- dashboard and API browsing of pipeline data
Observability
You can inspect outputs via:
GET /api/runs/:id/outputs- dashboard run detail
- dashboard data explorer for
run_outputs
Relationship to DAG execution
Output passing does not replace DAG validation; it complements it.
Typical pattern:
- use
depends_onorafter:<job>to define ordering - use outputs to pass the values needed by downstream steps
Without a dependency edge, output availability is not guaranteed.
Testing output passing
Relevant test coverage includes:
- capture modes in
internal/executor/output_test.go - cycle scoping in package tests and integration suites
- cross-package integration in
tests/phase4_integration_test.go
Run focused integration coverage with:
go test ./tests -run TestIntegration_OutputPassing_CycleIsolation -v