Skip to content

[Feature]: Compose workflows — run an installed workflow as a step #4680

Closed

Description

@markuswondrak

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

  1. Triage/routing meta-workflow dispatching to existing bug vs. speckit workflows.
  2. Reusing a shared sub-workflow across several parent workflows.

Acceptance Criteria

  • workflow step 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

  1. added
    triage-can-waitVerdict: valid and in-scope but deprioritized; held behind the evidence gate
    feature-assessRun the Spec Kit idea-assessment pipeline on this feature request
    on Sep 22, 2026
  2. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    Feature assessment — compose-workflows · Stage 1/5: Intake\n\n# Idea Intake: Compose workflows with nested workflow steps

    Idea (as captured)

    Workflows can orchestrate commands but not other workflows. Add a workflow
    step type that runs an already-installed workflow by ID, accepts an expression-
    resolved input mapping, exposes typed {status, run_id, steps} output, and
    deterministically resumes or reuses the child run. Reject unknown, path-like,
    disabled, and cyclic workflow references, enforce a maximum ancestry depth, and
    propagate failed or aborted child runs to the parent.

    The motivating use case is a routing/meta-workflow that dispatches to existing
    bug or speckit workflows without copying their steps or shelling out to
    specify workflow run. The issue identifies itself as a follow-up to
    discussion #4647 and says the maintainer approved that discussion.

    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

    • [NEEDS CLARIFICATION: Which workflow-run persistence and resume guarantees are required across process crashes and concurrent parent/child execution?]
    • [NEEDS CLARIFICATION: What exact target input-schema validation and expression evaluation behavior should nested calls use?]
    • [NEEDS CLARIFICATION: What maximum ancestry depth and run-ID length/collision policy are acceptable?]
    • [NEEDS CLARIFICATION: How should cancellation, timeout, disabled workflows, and child terminal statuses map to parent step statuses?]
    • [NEEDS CLARIFICATION: What user demand or workflow volume justifies the additional engine complexity?]

    Generated by 💡 Assess a Feature Request by Installing and Running Spec Kit for #4680 · copilot · gpt52codex · 3.92 AIC · ⌖ 5.65 AIC · ⊞ 24.2K · ◷

  3. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    Feature assessment — compose-workflows · Stage 2/5: Research\n\n# Idea Research: Compose workflows with nested workflow steps

    • Slug: compose-workflows
    • Created: 2026-09-23T13:19:00Z
    • Evidence confidence (overall): medium

    Users & Demand

    Prior Art

    • The current engine models a workflow as a parsed definition with a list of steps and validates step types from STEP_REGISTRY; the fallback registry contains control-flow and dispatch types but no workflow type — [source: src/specify_cli/workflows/engine.py] (confidence: high).
    • Step execution receives a StepContext containing inputs, accumulated step results, project root, run ID, and resume state, while StepResult exposes status, output, nested steps, and errors — [source: src/specify_cli/workflows/base.py] (confidence: high).
    • Installed workflow resolution and overlays are already separate concerns. Resolver tests reject unsafe IDs before source collection and validate base-plus-overlay definitions — [source: tests/workflows/test_resolver_integration.py; docs/reference/workflows.md] (confidence: high).
    • The CLI already documents independent 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).
    • The issue's alternatives are shelling out, copying steps/overlays, or redesigning generic nested resume; these are stated alternatives, not evaluated production results — [source: [Feature]: Compose workflows — run an installed workflow as a step #4680] (confidence: medium).

    Market & Context

    • Today, a user can run an installed workflow directly or compose an existing workflow by copying steps/using overlays; the issue reports that shelling out loses typed output and paused-run re-entry — [source: docs/reference/workflows.md; [Feature]: Compose workflows — run an installed workflow as a step #4680] (confidence: medium).
    • No external market or competitor research was requested or fetched. Adoption and alternatives outside this repository are [NEEDS CLARIFICATION].

    Data & Constraints

    • Workflow IDs use a lowercase alphanumeric/hyphen format, and resolver tests explicitly protect against path traversal and reserved IDs; nested dispatch must preserve those constraints — [source: src/specify_cli/workflows/engine.py; tests/workflows/test_resolver_integration.py] (confidence: high).
    • Workflow inputs currently support declared string, number, and boolean types; the proposal requires expression-resolved mappings to be validated against the target schema — [source: src/specify_cli/workflows/engine.py; [Feature]: Compose workflows — run an installed workflow as a step #4680] (confidence: high).
    • Run states include created, running, paused, completed, failed, and aborted, and resume is a documented operation; exact persistence atomicity and concurrency guarantees are not established by the available evidence — [source: src/specify_cli/workflows/base.py; docs/reference/workflows.md] (confidence: medium).
    • The proposal introduces ancestry tracking, deterministic child IDs, and a depth limit; no existing depth, ID namespace, timeout, or cancellation policy was found — [source: [Feature]: Compose workflows — run an installed workflow as a step #4680; repository search] (confidence: medium).

    Evidence Against the Idea

    Gaps & Open Questions

    • [NEEDS CLARIFICATION: Quantify current demand, duplicated workflow logic, and expected nested-run volume.]
    • [NEEDS CLARIFICATION: Define crash consistency and concurrent resume semantics for deterministic child IDs.]
    • [NEEDS CLARIFICATION: Define child status/error/cancellation propagation and timeout behavior.]
    • [NEEDS CLARIFICATION: Confirm schema validation and expression rules for child inputs and outputs.]
    • [NEEDS CLARIFICATION: Confirm the maximum depth and whether ancestry is persisted in every run state.]

    Sources

    Generated by 💡 Assess a Feature Request by Installing and Running Spec Kit for #4680 · copilot · gpt52codex · 3.92 AIC · ⌖ 5.65 AIC · ⊞ 24.2K · ◷

  4. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    Feature assessment — compose-workflows · Stage 3/5: Problem\n\n# Problem Definition: Compose workflows with nested workflow steps

    • Slug: compose-workflows
    • Created: 2026-09-23T13:19:00Z
    • Inputs used: intake.md + research.md

    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

    • Enable reusable composition of installed workflows without copying their steps.
    • Preserve structured child status and step results for downstream parent logic.
    • Make re-execution and paused child workflows deterministic and free of duplicate child runs.
    • Keep workflow-target selection bounded to safe, installed, enabled workflow IDs.

    Non-Goals

    • Redesigning generic workflow resume or replacing the existing run-state model.
    • Executing arbitrary filesystem paths, URLs, or uninstalled workflow definitions through the nested step.
    • Designing cross-process distributed orchestration, scheduling, or external workflow integrations.
    • Defining detailed engine architecture, schemas, or implementation tasks in this assessment.

    Success Metrics

    • A valid nested workflow can be selected by installed ID, receive validated inputs, and expose documented structured output (baseline: impossible as a first-class step).
    • Re-running a parent after a child pause/failure/crash yields one deterministic child run and either resumes or reuses it according to its state (baseline: no nested-run behavior).
    • Unknown, path-like, disabled, and cyclic targets are rejected before child execution (baseline: no nested target validation).
    • Parent outcomes faithfully represent failed and aborted child outcomes (baseline: shell-based composition is opaque).
    • [NEEDS CLARIFICATION: Target test coverage, performance ceiling, and acceptable run-state recovery guarantees.]

    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

    • [NEEDS CLARIFICATION: What observed demand or workflow volume warrants this engine complexity?]
    • [NEEDS CLARIFICATION: What exact input-schema, expression, output, timeout, and cancellation semantics are required?]
    • [NEEDS CLARIFICATION: What crash-consistency and concurrent-resume guarantees are mandatory?]
    • [NEEDS CLARIFICATION: What maximum ancestry depth and deterministic ID policy should be adopted?]

    Generated by 💡 Assess a Feature Request by Installing and Running Spec Kit for #4680 · copilot · gpt52codex · 3.92 AIC · ⌖ 5.65 AIC · ⊞ 24.2K · ◷

  5. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    Feature assessment — compose-workflows · Stage 4/5: Concept\n\n# Concept: Compose workflows with nested workflow steps

    • Slug: compose-workflows
    • Created: 2026-09-23T13:19:00Z
    • Recommended option: Bounded nested workflow step

    Options

    Option A — Minimal synchronous composition

    • Sketch: Add a workflow step that selects an installed workflow, passes resolved inputs, waits for completion, and returns a scoped result. Limit the first release to completed/failed children and defer paused-run recovery.
    • Appetite: medium
    • Trade-offs: Smallest useful composition surface and simpler failure behavior, but it does not solve the issue's central re-entry requirement and would create a second model for paused children.
    • Rabbit holes: Ambiguous handling of gates, child logs, cancellation, and parent interruption.

    Option B — Bounded nested workflow step

    • Sketch: Treat an installed workflow as a first-class child run with a deterministic identity derived from the parent step, validated input mapping, scoped typed output, persisted ancestry, and explicit propagation of paused, terminal, failed, and aborted states. Enforce safe installed IDs and a finite depth bound.
    • Appetite: large
    • Trade-offs: Addresses the stated routing, reuse, typed-output, and re-entry goals while reusing existing run concepts; requires careful persistence, resume, cycle, and concurrency semantics and carries materially higher engine risk.
    • Rabbit holes: Generic nested-resume redesign, distributed locking, arbitrary expression evaluation, cancellation trees, and unbounded output/log aggregation.

    Option C — Keep composition outside the engine

    • Sketch: Document shell-based orchestration and reusable workflow packaging as the supported composition boundary, with no nested step.
    • Appetite: small
    • Trade-offs: Lowest implementation risk and no new persistence semantics, but preserves opaque outputs, duplication, and the exact limitations motivating the request.
    • Rabbit holes: Users may build ad hoc wrappers that become unsupported APIs and inconsistent retry behavior.

    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)

    • Arbitrary path, URL, or uninstalled workflow execution.
    • Cross-project or distributed workflow orchestration.
    • A general-purpose nested-run framework beyond the single workflow step.
    • Automatic aggregation of child logs into parent output.
    • New scheduling, timeout, or cancellation systems beyond explicit mappings needed for the child step.

    Assumptions to Validate

    • Installed workflow IDs and existing resolver validation are the correct lookup boundary.
    • Existing persisted run state can support deterministic child lookup without a generic engine rewrite.
    • A finite ancestry depth is sufficient protection against cycles and runaway nesting.
    • Child outputs can be represented as a stable, scoped contract without exposing internal state.
    • Maintainers accept the large appetite because composition is a recurring workflow-author need.

    Generated by 💡 Assess a Feature Request by Installing and Running Spec Kit for #4680 · copilot · gpt52codex · 3.92 AIC · ⌖ 5.65 AIC · ⊞ 24.2K · ◷

  6. github-actions commented on Sep 23, 2026

    @github-actions
    Contributor

    Feature assessment — compose-workflows · Stage 5/5: Decision — verdict needs-clarification\n\n# Decision: Compose workflows with nested workflow steps

    • Slug: compose-workflows
    • Decided: 2026-09-23T13:19:00Z
    • Verdict: needs-clarification
    • Artifacts reviewed: intake.md + research.md + problem.md + concept.md

    Scorecard

    Criterion Rating Justification
    Problem validity adequate The issue describes a concrete limitation in the current workflow model, and repository evidence confirms workflows currently run as typed step lists without a nested workflow step.
    Evidence strength weak The request is detailed and maintainer-approved discussion is cited, but demand evidence is limited to one issue and one reaction with no usage, support, or duplication measurements.
    Value vs. inaction adequate The stated alternatives lose reuse, typed output, or paused-child re-entry, but the frequency and cost of those limitations are not quantified.
    Feasibility / appetite adequate Existing resolver, step registry, run statuses, and resume concepts provide plausible seams, but deterministic persistence and concurrency behavior are not yet bounded.
    Strategic fit unknown The repository clearly supports workflow automation, but no explicit constitution or prioritization evidence was found for this large engine feature.
    Risk posture weak Cycle checks are proposed, but crash consistency, concurrent resume, cancellation, timeouts, output stability, and status propagation remain open.

    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

    • Blocking questions:
      • [NEEDS CLARIFICATION: What observed usage, duplicated workflow examples, or user requests establish demand and priority?]
      • [NEEDS CLARIFICATION: What are the atomicity and concurrency guarantees when a parent or child crashes or is resumed twice?]
      • [NEEDS CLARIFICATION: How do paused, failed, aborted, timed-out, and cancelled child states map to parent step and run states?]
      • [NEEDS CLARIFICATION: What exact input-schema validation, expression, output-shape, and maximum-depth contracts are required?]
    • Revisit stage: research, then shape

    Generated by 💡 Assess a Feature Request by Installing and Running Spec Kit for #4680 · copilot · gpt52codex · 3.92 AIC · ⌖ 5.65 AIC · ⊞ 24.2K · ◷

  7. mnriem commented on Sep 23, 2026

    @mnriem
    Collaborator

    @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?

  8. markuswondrak commented on Sep 23, 2026

    @markuswondrak
    ContributorAuthor

    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

    • The composed execution has one RunState, one run_id, and one run directory.
    • A workflow step does not create or return a child run_id.
    • Included workflow state is persisted as a nested execution scope within the current run.
    • Before entering the included workflow, the engine persists the resolved target ID, validated inputs, workflow definition binding/snapshot, progress, and local step results.
    • On resume, the engine reuses that persisted binding instead of reevaluating the target or selecting a newer installed definition.
    • A process crash therefore uses the existing workflow resume mechanism; there is no separate parent/child recovery protocol.
    • Concurrent execution or resume of the same run remains unsupported, matching the current engine. This feature does not add locks, leases, or new cross-process guarantees.
    • The existing limitation also remains: resume tracks the enclosing top-level step rather than an exact nested step path. Persisted workflow bindings ensure replay cannot silently select a different included workflow.

    2. Status, error, timeout, and cancellation behavior

    Because composition uses one run, there are no child statuses to translate:

    Included subtree result Workflow step result Run behavior
    Completed COMPLETED Continue
    Paused PAUSED Pause the current run
    Failed FAILED Fail unless the workflow call has continue_on_error: true
    Gate abort FAILED with aborted: true Abort the current run
    Skipped steps No failure by itself Continue

    continue_on_error applies at two boundaries:

    • On an included step, it retains its existing meaning inside that workflow.
    • On the workflow step, it allows the caller to continue after an otherwise unhandled included-workflow failure.
    • It never overrides an explicit abort or pause.

    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 cancelled or timed-out run statuses.

    3. Target resolution, expressions, and input validation

    The workflow target may be literal or expression-selected:

    workflow: bugfix
    workflow: "{{ steps.triage.output.workflow }}"

    Rules:

    • The evaluated result must be a string that exactly matches a safe, registered, installed, enabled workflow ID.
    • No trimming, case conversion, paths, URLs, or arbitrary YAML files.
    • The target is resolved from the current project only.
    • The selected workflow definition is validated before execution.

    The call behaves like a typed function call:

    1. Require input to be a mapping with string keys.
    2. Evaluate each value once in the caller scope.
    3. Reject inputs not declared by the selected workflow.
    4. Apply child defaults.
    5. Reject missing required inputs.
    6. Apply the existing string, number, boolean, and enum validation.
    7. Expose only those resolved inputs inside the included workflow.

    The included workflow has an isolated scope:

    • It cannot access caller inputs that were not explicitly mapped.
    • It cannot access caller step results.
    • Its steps namespace contains only its own local results.
    • Existing built-in and custom steps work unchanged inside that scope.

    4. Output contract

    Included workflows explicitly declare values that may cross back to the caller:

    outputs:
      result:
        value: "{{ steps.fix.output.stdout }}"
      tested:
        value: "{{ steps.test.output.exit_code == 0 }}"

    The caller reads them directly from the workflow step:

    "{{ steps.run-bugfix.output.result }}"
    "{{ steps.run-bugfix.output.tested }}"

    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, and error.

    5. Cycles, depth, and ID collisions

    • Recursive composition is supported.
    • The engine tracks the active workflow-ID path.
    • A target already on that path is rejected as a cycle.
    • The check is path-based, so diamonds remain legal.
    • The root workflow is depth 0; at most 16 included-workflow levels are allowed.
    • The engine rejects a call before entering depth 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:

    • No child runs
    • No deterministic child run IDs
    • No parent/child concurrency model
    • No cancellation tree
    • No raw included-state exposure
    • No cross-project execution
    • No subprocess call to specify workflow run

    The 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.

  9. mnriem commented on Sep 23, 2026

    @mnriem
    Collaborator

    Go for it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-assessRun the Spec Kit idea-assessment pipeline on this feature requestfeature-needs-clarificationFeature assessment verdict: needs clarificationtriage-can-waitVerdict: valid and in-scope but deprioritized; held behind the evidence gate

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions