Skip to content

Connecting pipelines, graphs and scripts

A workflow is a pipeline, a graph or a script, and any of them can start any other. A pipeline can run a graph you built on the canvas as one of its jobs, a graph can run a pipeline or a script as one of its steps, and a script can call a stored graph and use what it returns. A step can also compute a whole graph and have the run execute it. This page shows each connection with a short example, and how input and output cross between them. Composition has the full reference.

The work Write it as
Builds and tests across agents, kept next to the code a pipeline
Steps people edit and inspect on the canvas, or connector calls a graph
Control flow that reads better as code: loops, retries, decisions over data a script
Something only known once the run has looked at its input a step that generates a graph or pipeline
A reaction to something another workflow did, with no answer needed an event and a trigger or handler

Mix them where the work changes shape: a pipeline that hands its deploy to a graph the ops team owns, or a script that loops over regions and calls the same graph for each.

A workflow: job names the graph and passes its input. Store this graph first:

build-image.yaml
version: hyperspool-graph/v1
name: build-image
input_schema:
properties:
service: { type: string }
required: [service]
nodes:
- id: build
type: shell
config:
run:
$template: "echo building {{ input.service }} && spool output set tag={{ input.service }}-1.4.2"
outputs:
image: { $template: "registry.example.com/{{ nodes.build.output.tag }}" }
Terminal window
spool workflow set build-image build-image.yaml --publish

Then the pipeline:

name: release
jobs:
pick:
outputs:
service: service
steps:
- name: pick
run: spool output set service=api
image:
depends_on: [pick]
workflow:
graph: { name: build-image }
input:
service: "${{ steps.pick.output.service }}"
outputs:
image: image
deploy:
depends_on: [image]
steps:
- name: deploy
run: echo "deploying ${{ steps.image.output.image }}"

spool submit release.yaml -f prints deploying registry.example.com/api-1.4.2. The job’s input: can read earlier jobs through ${{ }}, the graph checks it against its input_schema, and the job’s outputs: picks keys from what the graph’s outputs: block declares. The graph runs its published version; graph: { name: build-image, version: 3 } pins one.

A workflow step (Sub-workflow on the canvas palette) with type: runs a pipeline or a script as a child. For a pipeline, input is the pipeline document; for a script, it is {script: ...}:

version: hyperspool-graph/v1
name: nightly
nodes:
- id: tests
type: workflow
config:
type: hyperspool/pipeline/v1
input: |
outputs:
passed: "${{ steps.unit.output.passed }}"
jobs:
unit:
outputs:
passed: passed
steps:
- name: unit
run: echo "running unit tests" && spool output set passed=212
- id: report
type: workflow
config:
type: hyperspool/script/v1
input:
script:
$template: |
#!/usr/bin/env bash
set -eu
spool step publish -- echo "publishing report: {{ nodes.tests.output.passed }} passed"
spool output set published=yes
edges:
- { from: tests, to: report }
outputs:
passed: { $expr: nodes.tests.output.passed }
published: { $expr: nodes.report.output.published }

After spool workflow set nightly nightly.yaml --publish and spool submit -w nightly, the run’s output is {"nodes": {...}, "passed": 212, "published": "yes"}. The pipeline child returns what its top-level outputs: declares. The script child returns a flat object: exit_code, duration_ms and every key it set with spool output set.

The script’s $template fills in a value from the graph before the script starts. A pipeline’s text can’t take the same treatment: the graph would try to resolve the pipeline’s own ${{ }} holes as well, and fail. To hand values to a pipeline, keep it in a repo and run it with from_repo: and input:, which the pipeline reads as input.*. To run another stored graph, use graph: { name: ... } as in the node reference.

spool call -w starts the stored graph, waits for it holding no agent, and prints its output as JSON:

#!/usr/bin/env bash
set -eu
out="$(spool call -w build-image --input '{"service": "web"}')"
echo "build-image returned: $out"
image="$(node -p 'JSON.parse(process.argv[1]).image' "$out")"
spool step deploy -- echo "deploying $image"
spool output set image="$image"
build-image returned: {"image":"registry.example.com/web-1.4.2","nodes":{"build":"success"}}
deploying registry.example.com/web-1.4.2

The call is a checkpoint, so a replay of the script gets the recorded output instead of starting the graph again. A child that fails makes spool call exit non-zero, which under set -e fails the script. The managed environments have node and python3 for reading the JSON, and no jq. spool spawn starts a run without waiting, and the language SDKs have call_graph; see Script commands.

When the steps aren’t known until the run has looked at something, one step writes a graph document and a workflow step runs it through definition::

version: hyperspool-graph/v1
nodes:
- id: plan
type: code
config:
script: |
set -eu
nodes=""
for region in eu-west us-east; do
nodes+="{\"id\": \"deploy_${region//-/_}\", \"type\": \"shell\", \"config\": {\"run\": \"echo deploy to $region\"}},"
done
spool output set --values "{\"graph\": {\"version\": \"hyperspool-graph/v1\", \"nodes\": [${nodes%,}]}}"
- id: deploy
type: workflow
config:
definition: { $expr: nodes.plan.output.graph }
edges:
- { from: plan, to: deploy }

The generated graph runs as a child run with one step per region. In a pipeline or a script, the same idea is a script: job that writes a file and runs spool call <file>, which takes a graph, a pipeline or a script. Build only what changed works through a full example, and A child computed at run time covers the limits.

A child run is a call: the parent waits and gets the result. An event is a notice: whoever emits it carries on, and anything listening starts or resumes on its own. Use one when the emitter shouldn’t know or wait for what reacts.

A pipeline job emits:

name: publish
jobs:
build:
steps:
- name: build
run: |
spool event emit image.built --payload '{"image": "registry.example.com/api-1.4.2"}'

A graph with a trigger step starts on it, with the event as its run input:

version: hyperspool-graph/v1
name: on-image
nodes:
- id: built
type: trigger
config: { event: image.built }
- id: scan
type: shell
config:
run: { $template: "echo scanning {{ input.payload.image }}" }
edges:
- { from: built, to: scan }

Publishing the graph registers it (spool workflow set on-image on-image.yaml --publish); each image.built then starts a run that prints scanning registry.example.com/api-1.4.2. A script or a pipeline reacts the same way through a handler, spool handler set scan ./scan.sh --on image.built, and a run that’s already going can wait for the event instead. Nothing flows back to the emitter. Events and handlers covers filters, subjects and the limits on chains of handlers.

Caller Passes input with Reads the result as
Pipeline workflow: job input:, with ${{ }} over earlier jobs outputs: on the job, then ${{ steps.<job>.output.<key> }}
Graph workflow step input:, with $expr and $template nodes.<id>.output.<key>
Script spool call -w <name> --input <json> the JSON spool call prints
Anything, by event the event’s payload nothing comes back; the reaction reads input.payload

What comes back is what the child declares: a graph’s or pipeline’s top-level outputs: block, a script’s spool output set keys. Only a successful child returns anything, and a failed child fails the step that started it. Every child has its own run page that links back to its parent, and can be rerun on its own.