Composition
A stage's `uses:` field can reference a **published pipeline** instead of a SKILL. The
Composition: uses: accepts pipeline IDs (v0.3+)
A stage’s uses: field can reference a published pipeline instead of a SKILL. The
referenced pipeline’s stages are fetched and inlined into the parent pipeline at
invocation time, with id-prefixing so they cannot collide with parent stage ids.
Syntax
stages:
- id: refactor
uses: code_rewriter@0.2.1
- id: research
uses: pipeline:web-research-helper@1.2.0
- id: enrich
uses: pipeline:enrichment-helper
The pipeline: prefix (COMPOSITION_USES_PREFIX in code) is what disambiguates a
pipeline ref from a skill ref. The parser (tryParsePipelineUses) returns null
for any uses: string that does not start with pipeline:, leaving it for the
skill-loader to handle.
Identifier grammar
| Component | Format |
|---|---|
| Prefix | literal pipeline: |
| Pipeline id | YAML stage-id grammar: [A-Za-z_][A-Za-z0-9_-]* |
| Version pin | @<semver> after the id (e.g. @1.2.0); absent = loose pin (latest) |
Resolution semantics
When runPipelineV2 encounters a stage with uses: pipeline:NAME@VERSION, it:
- Calls
tryParsePipelineUses→{ id, version? }ornull - Calls
resolvePipelineRef(ref, fetcher)→ fetcher hits the marketplace API - If fetcher returns
null→ throwsPipelineFetchError(carriesid+version) - Calls
expandComposition(child.stages, fetcher, depth + 1)— recursive injectNestedStagesprefixes each child stage id with<parent_id>.<child_id>- Non-dotted
depends_onentries in the child are rewritten to use the prefix; already-dotted paths (referencing sibling refs) are preserved verbatim
Depth limit
MAX_COMPOSITION_DEPTH = 4 (hard-coded constant). At depth 4 the expander throws
CompositionDepthError. This guards against malicious or accidental cycles
(A → B → A → B → ...) and accidental exponential blow-up.
if (depth >= MAX_COMPOSITION_DEPTH) {
throw new CompositionDepthError(depth);
}
2-level nesting example
stages:
- id: classify
provider: openai
model: gpt-4o-mini
prompt: "Classify: {{ inputs.text }}"
- id: extract
provider: openai
model: gpt-4o-mini
depends_on: [classify]
prompt: "Extract entities from class={{ stages.classify.outputs.category }}"
stages:
- id: preprocess
provider: openai
prompt: "Trim: {{ inputs.raw }}"
- id: research # will be expanded to: research.classify, research.extract
uses: pipeline:helper@1
depends_on: [preprocess]
- id: synthesize
provider: openai
depends_on: [research.extract]
prompt: "Synthesize from {{ stages.research.extract.outputs.entities }}"
After expansion the parent has 4 stages: preprocess, research.classify,
research.extract, synthesize. Downstream stages reference expanded ids via the
full dotted path (stages.research.extract.outputs.entities).
Recursion / cycle detection
The depth limit (MAX_COMPOSITION_DEPTH = 4) is the only cycle guard — there is
no per-pipeline visited-set. A chain pipeline:A → pipeline:B → pipeline:A is
blocked because depth 4 is reached before re-entering A. For practical use cases
(a pipeline references its helper once or twice), depth 4 is more than enough.
When does the fetch happen?
At invoke-time, via the optional fetchPipeline dependency injected into
runPipelineV2. The marketplace server implements this fetcher against its
pipeline registry (POST /v1/pipelines/:id). The stored YAML in D1 is not
pre-expanded at publish-time — original uses: pipeline:NAME@VERSION syntax is
preserved verbatim in provenance for reproducibility.
This means:
- Author can re-publish a referenced pipeline; consumers always get the latest unless version is pinned
- Marketplace controls access (private / granted_to / public visibility)
- Network failure on referenced pipeline →
PipelineFetchError→ pipeline aborts and buyer is refunded (same failure model as skill fetch)
Behavioral oracle
packages/pipeline-runtime/src/composition.ts— implementation:tryParsePipelineUses,resolvePipelineRef,injectNestedStages,expandComposition,CompositionDepthError,PipelineFetchError,MAX_COMPOSITION_DEPTH = 4,COMPOSITION_USES_PREFIX = 'pipeline:'packages/pipeline-runtime/tests/composition.test.ts— parser + ref-resolver + injector unit tests (prefix correctness,depends_onrewriting, immutability)packages/pipeline-runtime/tests/composition-expand.test.ts— expander: single-level inlining, recursive expansion,MAX_COMPOSITION_DEPTHenforcement,PipelineFetchErrormetadata, mixed composition + plain stagespackages/pipeline-runtime/tests/integration-r10-x.test.ts— end-to-end throughrunPipelineV2withfetchPipelineinjected: full DAG build from composed spec, fail-fast on missing referenced pipeline