Skip to content

Commit 862d10b

Browse files
author
Markus
committed
fix(workflows): address second review pass on workflow composition
- Add a downstream consumption test: a parent step after the workflow call evaluates `{{ steps.call.output.echoed }}`, so a failure to publish the completed child's output into the caller context before continuing is now caught (final-state inspection alone could not). - Update the built-in step inventories from 12 to 13 and add the `workflow` step to workflows/README.md and workflows/ARCHITECTURE.md. - Drop the two dangling references to spec/workflow_composition/ (the design decisions and implementation plan are not part of this change). Assisted-by: opencode (model: deepseek-v4.1-flash, supervised)
1 parent 2c4add6 commit 862d10b

4 files changed

Lines changed: 39 additions & 6 deletions

File tree

‎src/specify_cli/workflows/composition.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Workflow composition — scoped subtree execution helpers.
22
3-
Implements the decisions recorded in
4-
``spec/workflow_composition/design_decisions.md``:
3+
Implements the workflow composition decisions:
54
65
- reserved output names and the composition depth limit,
76
- registry-backed, installed-and-enabled target resolution,

‎tests/workflows/test_workflow_composition.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""Tests for workflow composition (the built-in ``type: workflow`` step).
22
33
Covers the composition helpers, engine scoped execution, strict input binding,
4-
persistence/resume, and CLI reporting. See
5-
``spec/workflow_composition/implementation_plan.md``.
4+
persistence/resume, and CLI reporting.
65
"""
76

87
from __future__ import annotations
@@ -301,6 +300,10 @@ def _parent_and_child(self, project_dir):
301300
"workflow": "child",
302301
"input": {"declared": "{{ inputs.shared }}"},
303302
},
303+
_shell(
304+
"consume",
305+
"echo {{ steps.call.output.echoed | default('MISSING') }}",
306+
),
304307
],
305308
inputs={"shared": {"type": "string", "default": "secret"}},
306309
),
@@ -322,6 +325,14 @@ def test_caller_cannot_see_child_locals(self, project_dir):
322325
# Only declared outputs + stable metadata cross the boundary.
323326
assert set(call_output) == {"workflow", "status", "echoed"}
324327

328+
def test_caller_can_consume_child_output_downstream(self, project_dir):
329+
"""The caller context must publish a completed child's output *before*
330+
the next step runs, so a downstream expression can consume it."""
331+
self._parent_and_child(project_dir)
332+
state = _run(project_dir, "parent")
333+
assert state.status == RunStatus.COMPLETED
334+
assert state.step_results["consume"]["output"]["stdout"].strip() == "secret"
335+
325336

326337
class TestPublicOutputShapes:
327338
def test_completed_shape(self, project_dir):

‎workflows/ARCHITECTURE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ When a `gate` step pauses execution, the engine persists `current_step_index` an
7979
8080
## Step Types
8181

82-
The engine ships with 12 built-in step types, each in its own subpackage under `src/specify_cli/workflows/step/`:
82+
The engine ships with 13 built-in step types, each in its own subpackage under `src/specify_cli/workflows/step/`:
8383

8484
| Type Key | Class | Purpose | Returns `next_steps`? |
8585
|----------|-------|---------|-----------------------|
@@ -95,6 +95,7 @@ The engine ships with 12 built-in step types, each in its own subpackage under `
9595
| `do-while` | `DoWhileStep` | Loop, always runs body at least once | Yes (always) |
9696
| `fan-out` | `FanOutStep` | Dispatch per item over a collection | No (engine expands) |
9797
| `fan-in` | `FanInStep` | Aggregate results from fan-out | No |
98+
| `workflow` | `WorkflowStep` | Run an installed workflow as a scoped subtree | No |
9899

99100
## Step Registry
100101

‎workflows/README.md‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ The bundled `speckit` workflow only declares `spec` (and optional
8585

8686
## Step Types
8787

88-
Workflows support 12 built-in step types:
88+
Workflows support 13 built-in step types:
8989

9090
### Command Steps (default)
9191

@@ -313,6 +313,28 @@ Aggregate results from fan-out steps:
313313
output: {}
314314
```
315315

316+
### Workflow Steps
317+
318+
Compose an installed workflow as a scoped subtree of the current run — a
319+
function call with one run state and one process. Values cross the boundary
320+
only through the target's declared `inputs` and `outputs`:
321+
322+
```yaml
323+
- id: run-selected
324+
type: workflow
325+
workflow: "{{ inputs.target }}" # installed, registered, and enabled
326+
input:
327+
report: "{{ inputs.report }}"
328+
```
329+
330+
The `workflow` value must resolve to something the engine captures — a declared
331+
workflow input or a preceding step's captured output such as a `shell` step's
332+
`stdout`. A `prompt` step streams its output and returns an empty `stdout`, so
333+
it cannot drive `workflow` selection. The child receives only the bound declared
334+
inputs, and only its declared outputs (merged with `workflow` and `status`) are
335+
published back to the caller. See
336+
[Workflow composition](../docs/reference/workflows.md#workflow-composition-type-workflow).
337+
316338
## Error Handling
317339

318340
By default, any step that returns `StepResult(status=StepStatus.FAILED, ...)`

0 commit comments

Comments
 (0)