# Spec: Value Resolution Steps ## Status Implemented. ## Scope This spec defines `${steps.step_id.outputs.name}` references used by value resolution. Common reference syntax, supported fields, and string insertion are defined by [Spec 003: Value Resolution and Field Evaluation](003-value-resolution.md). Resolution timing is defined by [Spec 003: Value Resolution and Field Evaluation](003-value-resolution.md). Step identity is defined by [Spec 009: Step Reference](009-step-reference.md). Step output declaration and publication are defined by [Spec 012: Step Outputs](012-step-outputs.md). This spec does not define dependency resolution mechanics, step execution, or output file parsing; it only defines dependency constraints for fields that reference step outputs. ## Goal Later fields can reference outputs published by completed steps. ## Motivation Step outputs are produced during execution, so they are not available when the workflow file is loaded. A step-output reference must therefore describe both a static relationship between steps and a runtime lookup after the producing step completes. This spec keeps those rules in one place. References use step ids. A reference resolves only when the producing step is ordered before the consuming step. If the value is unavailable, Dagu preserves the original reference text. Explicit inspection surfaces report a passive notice for that preserved reference. ## Behavior ### Reference Form - An unescaped `${steps.step_id.outputs.name}` reads `name` from a completed step output. - The reference form is exact. - It has exactly four path segments: `steps`, `step_id`, `outputs`, and `name`. - Nested output paths such as `${steps.build.outputs.metadata.tag}` are unsupported braced text. - `step_id` must identify an existing step `id`. - `outputs` is a literal path segment. - `name` must identify an output value published by the referenced step. - When the referenced step declares an output contract, `name` must be declared by that contract. - `step_id` and `name` must match `^[A-Za-z][A-Za-z0-9_]*$`. - Escaped `\${steps.step_id.outputs.name}` is ordinary string content under Spec 003. - Escaped step-output-looking text must not be resolved. - Escaped step-output-looking text must not produce a passive notice. - Unsupported braced text such as `${step.outputs.name}`, `${steps.foo.bar}`, `${steps.build.outputs.metadata.tag}`, `${steps.build-step.outputs.image}`, `${steps.1build.outputs.image}`, `${build.output.image}`, and `${foo.steps.outputs.name}` is outside this spec. - Unsupported braced text is preserved silently by Spec 003. ### Dependency Rules - A step output reference can resolve only when the owning step depends directly or transitively on the producing step, through `depends` or through an inferred dependency. - A step output reference to the owning step cannot resolve. - A field without an owning step cannot resolve step outputs unless another spec explicitly allows that field to wait for step completion. - Handler fields do not have step-output lookup scope for this spec. ### Runtime Lookup - A step output reference may resolve only after the referenced step completes and publishes the output. - A step-owned field may resolve a step output reference only when the referenced output is available before the owning step starts. - Step output values are inserted into string fields according to Spec 003 string insertion rules. - Step output references read the named outputs a step publishes, whatever mechanism publishes them. - A step publishes named outputs through its top-level `outputs` contract and `DAGU_OUTPUT_FILE`, through `output_schema`, through object-form `output`, through `stdout.outputs`, or through an action that publishes named outputs. - A failed attempt publishes no outputs, so its references stay unresolved. - Step output references do not read string-form `output: VAR`, which captures a variable rather than a named output, nor stdout, stderr, logs, artifacts, or nested output paths. ### Foreach Body Scope Spec 018 adds `foreach.steps` body scopes. Rules: - A `foreach` body step can reference top-level step outputs when the top-level producer is ordered before the owning `foreach` step. - A `foreach` body step can reference an earlier body step output from the same item body. - Body step output references never cross item bodies. - A top-level step after a `foreach` step cannot reference body step outputs directly. - A top-level step after a `foreach` step must consume the `foreach` aggregate output when it needs values collected from body steps. - `foreach.collect` expressions resolve after a successful item body with item scope and that item body's step-output scope available. ### Inferred Dependencies A supported step output reference is an unambiguous ordering signal: without the dependency the reference can only preserve as text. Dagu therefore adds the dependency. Rules: - A supported `${steps..outputs.}` reference in a value-resolution field adds an inferred dependency from the producing step to the top-level step that owns the field. - Only the reference forms this spec defines as supported create an inferred dependency. Escaped text, unsupported braced text, `${env.}`, `$NAME`, and string-form `output: VAR` variables create none. - Only fields with step-output lookup scope create an inferred dependency. A field whose notice reason would be `namespace_unavailable` creates none. `handler_on` steps have no position in the step graph and create none. - A template executor body is rendered as written and creates none. Other template executor fields, such as `with.data`, create one. - A reference inside `foreach.steps` to a top-level step attaches the dependency to the owning top-level `foreach` step. A reference inside `foreach.steps` to another body step of the same body creates no top-level dependency. - Item-scoped `foreach.key` and `foreach.collect` fields create none. - Explicit `depends` and inferred dependencies are unioned. An explicit empty `depends` sequence does not suppress inferred dependencies. - A reference to a producer the owning step already depends on, directly or transitively, adds nothing. - A reference to an unknown step id or to the owning step adds nothing and keeps its passive notice. - The combined graph must be acyclic. An inferred dependency that would close a cycle is a load error that names the producing step and the owning step. - In a chain DAG, chain order already satisfies every inferred dependency that points to an earlier step. A reference to a later step is the cycle case. - Inferred dependencies order execution. Behavior that `depends` enables beyond ordering, such as chat message inheritance between LLM steps, still requires an explicit `depends` entry. - Inferred dependencies are stored on the step separately from `depends`. `dagu validate` reports each one as `inferred: -> ()`, the API carries them in a field separate from `depends`, and the UI graph draws them with a distinct line style. ### Validation - An unresolved supported step-output reference in a value-resolution field must preserve the original reference text. - An unresolved supported step-output reference is not a validation error by itself. - Explicit inspection surfaces must report a passive notice for each preserved supported step-output reference. - Each passive notice must identify the owning field path, the original reference text, and the reason. - Reason values are: | Reason | Meaning | | --- | --- | | `unknown_step_id` | The referenced step id does not identify a step. | | `unknown_output_name` | The referenced step is known, but the output name is not declared by an available output contract. | | `self_reference` | The owning step references its own output. | | `namespace_unavailable` | The owning field has no step-output lookup scope in the current phase. | - An unknown `steps.` reference must use reason `unknown_step_id`. - An unknown `steps..outputs.` reference must use reason `unknown_output_name` when the referenced step is known, publishes a statically known output contract, and the referenced output name is not in it. - A step whose published output names are known only during a run has no statically known contract, so a reference to it must not use reason `unknown_output_name`. A sub-DAG step and an action step resolved from a manifest publish such names. - A step output reference to the owning step must use reason `self_reference`. - A step output reference in a field with no step-output lookup scope must use reason `namespace_unavailable`. - A `namespace_unavailable` step-output notice is a defect because the owning field cannot resolve a step output during a run. - An unavailable step output value must preserve the original reference text before the owning field is used. - For step-owned fields, runtime value-resolution misses must preserve before the owning step starts. - When more than one notice reason could apply, Dagu reports the first matching reason in this order: 1. `namespace_unavailable` 2. `unknown_step_id` 3. `self_reference` 4. `unknown_output_name` - Escaped step-output-looking text must not produce a passive notice. - Unsupported braced text must not produce a passive notice. ## Examples Valid step output reference: ```yaml steps: - id: build run: | printf 'image=v1.2.3\n' >> "$DAGU_OUTPUT_FILE" outputs: - name: image - id: deploy depends: build env: IMAGE: ${steps.build.outputs.image} run: ./deploy.sh "$IMAGE" ``` Inferred dependency: ```yaml steps: - id: build run: | printf 'image=v1.2.3\n' >> "$DAGU_OUTPUT_FILE" outputs: - name: image - id: deploy run: echo ${steps.build.outputs.image} ``` `deploy` runs after `build` and the reference resolves to `v1.2.3`. `dagu validate` reports `inferred: build -> deploy (steps[1].run)`. Inferred dependency cycle: ```yaml steps: - id: a run: echo ${steps.b.outputs.y} outputs: - name: x - id: b run: echo ${steps.a.outputs.x} outputs: - name: y ``` Loading fails with an error naming `a` and `b`. Literal embedded code: ```yaml steps: - id: script run: | node - <<'JS' console.log('\${steps.build.outputs.image}') JS ``` Dagu passes `${steps.build.outputs.image}` to the later JavaScript interpreter as code text. The JavaScript single-quoted string then treats it as literal text.