Repository navigation
[Feature]: Compose workflows — run an installed workflow as a step #4680
Description
Problem Statement
Workflows can orchestrate commands but not other workflows. There is no workflow step type and overlays only edit a single base workflow, so a meta-workflow that routes to different workflows must copy their steps or shell out to specify workflow run opaquely.
Proposed Solution
Add a workflow step type that runs an already-installed workflow by ID:
- id: route
type: if
condition: "{{ steps.triage.output.stdout | contains('bug') }}"
then:
- id: run-bug
type: workflow
workflow: bug # installed workflow id
input: { spec: "{{ inputs.spec }}" }
else:
- id: run-speckit
type: workflow
workflow: speckit # installed workflow id
input: { spec: "{{ inputs.spec }}" }Installed IDs only. The step starts the sub-workflow as its own run and exposes typed output:
{ "status": ..., "run_id": ..., "steps": {<child step id>: <child step_result>} }downstream as {{ steps.run-bug.output.steps.<child_id>.output... }} (scoped under the parent step id, so no collision with other top-level step results).
Re-entry: the child run_id is deterministic — sanitize(f"{parent_run_id}-{effective_step_id}") (the effective id already carries loop/fan-out iteration). On re-execution the step looks up that run: paused/failed → engine.resume(child_run_id); terminal → reuse the persisted result; absent → start it. A sub-workflow that pauses at a gate therefore resumes from its own top-level state, with no duplicate runs even if the parent crashed before recording output.
Cycle safety: each run carries a path-based workflow_ancestry; the step refuses a child already on the path (or exceeding a max depth) and passes ancestry + [current_id] down — diamonds stay legal, only true cycles are blocked. Checked at runtime since target IDs may be expressions.
Alternatives Considered
- Shell step calling
specify workflow run --json— opaque, loses typed output, can't re-enter a paused sub-run. - Copying steps / overlays — duplicates logic and drifts.
- Generic nested-resume via the top-level step index — engine-wide redesign; sidestepped by the deterministic run-id handoff.
Component
Specify CLI (initialization, commands)
AI Agent
Not applicable
Use Cases
- Triage/routing meta-workflow dispatching to existing
bugvs.speckitworkflows. - Reusing a shared sub-workflow across several parent workflows.
Acceptance Criteria
-
workflowstep type registered, documented, and tested. - Resolves installed IDs only; rejects unknown, path-like, and disabled workflows.
-
input:supports expressions and validates against the target schema. - Output
{status, run_id, steps}exposed and readable downstream. - Deterministic child run_id; paused/failed child resumed via
engine.resume; terminal child reused; no duplicate runs after a crash. - Path-based cycle detection plus max-depth backstop.
- Failed/aborted child surfaces as a failed/aborted parent step.
Additional Context
Follow-up to discussion #4647 (maintainer approved).
AI Disclosure
Created with opencode (model: deepseek-v4.1-flash, human-supervised) on behalf of @markuswondrak, based on the AI-assisted draft in discussion #4647.
Activity
github-actions commentedon Sep 23, 2026
Feature assessment — compose-workflows · Stage 1/5: Intake\n\n# Idea Intake: Compose workflows with nested workflow steps
Idea (as captured)
Restated
The proposal is to let an installed workflow invoke another installed workflow
as a first-class step, with scoped results and restart-safe child-run behavior.
This would support reusable routing and composition without duplicating workflow
definitions or using opaque shell subprocesses.
Origin & Context
First-Glance Unknowns
github-actions commentedon Sep 23, 2026
Feature assessment — compose-workflows · Stage 2/5: Research\n\n# Idea Research: Compose workflows with nested workflow steps
Users & Demand
triage-can-waitlabel; no comments or usage metrics were available in the issue response — [source: [Feature]: Compose workflows — run an installed workflow as a step #4680] (confidence: medium).Prior Art
STEP_REGISTRY; the fallback registry contains control-flow and dispatch types but noworkflowtype — [source: src/specify_cli/workflows/engine.py] (confidence: high).StepContextcontaining inputs, accumulated step results, project root, run ID, and resume state, whileStepResultexposes status, output, nested steps, and errors — [source: src/specify_cli/workflows/base.py] (confidence: high).workflow run,workflow resume, and persisted run status, but the documented JSON output is a top-level run result rather than a nested step result — [source: docs/reference/workflows.md] (confidence: high).Market & Context
Data & Constraints
Evidence Against the Idea
Gaps & Open Questions
Sources
github-actions commentedon Sep 23, 2026
Feature assessment — compose-workflows · Stage 3/5: Problem\n\n# Problem Definition: Compose workflows with nested workflow steps
Problem Statement
Workflow authors who need to route or reuse an existing workflow must currently
duplicate its steps, rely on overlays, or invoke the CLI opaquely. This makes
composition harder to maintain and prevents a parent workflow from treating a
child run's status and structured results as part of its own control flow.
Affected Users & Stakeholders
Goals
Non-Goals
Success Metrics
Cost of Inaction
Authors continue to copy workflow logic or shell out to the CLI. Copied definitions can drift, while shell composition lacks typed output and does not provide the requested paused-child re-entry behavior (source: issue #4680). The frequency and business impact of this cost are not measured.
Open Questions
github-actions commentedon Sep 23, 2026
Feature assessment — compose-workflows · Stage 4/5: Concept\n\n# Concept: Compose workflows with nested workflow steps
Options
Option A — Minimal synchronous composition
Option B — Bounded nested workflow step
Option C — Keep composition outside the engine
Recommendation
Recommend Option B only as a bounded, explicitly specified capability. It is the
only option that meets the issue's stated typed-output and paused-child
re-entry goals. Before specification, maintainers should confirm demand and
define crash consistency, concurrency, cancellation, and schema semantics; if
those questions cannot be answered, defer rather than implement Option A as an
incomplete substitute.
Out of Scope (for the recommended option)
Assumptions to Validate
github-actions commentedon Sep 23, 2026
Feature assessment — compose-workflows · Stage 5/5: Decision — verdict needs-clarification\n\n# Decision: Compose workflows with nested workflow steps
Scorecard
Verdict & Rationale
The idea is promising and technically aligned with the workflow engine, but the
assessment cannot honestly reach go: the decision rules require at least
adequate evidence strength, and the available demand signal is thin. The
recommended bounded concept is plausible, yet several persistence and lifecycle
contracts are unresolved. Answer the blocking questions below, then revisit
research and shape before specification.
If needs-clarification
mnriem commentedon Sep 23, 2026
@markuswondrak I like the idea, but we probably need to answer some of the clarification questions from above. You can ignore the demand ones, but the architectual ones we should answer. What do you think?
markuswondrak commentedon Sep 23, 2026
Thanks, that makes sense. We can ignore the demand question and answer the architectural points by tightening the proposal.
The key clarification is that an included workflow will not start a separate child run. It will execute as a scoped subtree of the current workflow run.
1. Persistence, crashes, and concurrent resume
RunState, onerun_id, and one run directory.workflowstep does not create or return a childrun_id.2. Status, error, timeout, and cancellation behavior
Because composition uses one run, there are no child statuses to translate:
COMPLETEDPAUSEDFAILEDcontinue_on_error: trueFAILEDwithaborted: truecontinue_on_errorapplies at two boundaries:workflowstep, it allows the caller to continue after an otherwise unhandled included-workflow failure.No new timeout or cancellation model is introduced. A command timeout remains a normal failed step, graceful interruption retains current pause behavior, and the engine still has no separate
cancelledortimed-outrun statuses.3. Target resolution, expressions, and input validation
The
workflowtarget may be literal or expression-selected:Rules:
The call behaves like a typed function call:
inputto be a mapping with string keys.string,number,boolean, andenumvalidation.The included workflow has an isolated scope:
stepsnamespace contains only its own local results.4. Output contract
Included workflows explicitly declare values that may cross back to the caller:
The caller reads them directly from the workflow step:
A completed result is shaped like:
{ "workflow": "bugfix", "status": "completed", "result": "...", "tested": true }Only declared outputs cross the boundary. Included inputs, raw step records, logs, stdout/stderr, and internal persisted state remain private. Failed calls expose stable metadata such as
workflow,status, anderror.5. Cycles, depth, and ID collisions
0; at most 16 included-workflow levels are allowed.17.There is no child run-ID length or collision policy because included workflows do not create child runs.
Persisted execution follows a nested scope tree rather than flattening included steps into encoded global IDs. Step IDs stay local to each scope, preventing collisions without introducing delimiter or escaping rules.
Revised scope
This is therefore a scoped single-run composition feature, not nested run orchestration:
specify workflow runThe required engine addition is scoped subtree execution: the included workflow runs with private inputs and step results while sharing the current run persistence and lifecycle.
Posted on behalf of @markuswondrak by OpenCode (model: github-copilot/gpt-5.6-sol, autonomous); comment fully AI-drafted.
mnriem commentedon Sep 23, 2026
Go for it!