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.
Which form to pick
Section titled “Which form to pick”| 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 pipeline runs a stored graph
Section titled “A pipeline runs a stored graph”A workflow: job names the graph and passes its input. Store this graph
first:
version: hyperspool-graph/v1name: build-imageinput_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 }}" }spool workflow set build-image build-image.yaml --publishThen 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 graph runs a pipeline or a script
Section titled “A graph runs a pipeline or a script”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/v1name: nightlynodes: - 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=yesedges: - { 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.
A script calls a stored graph
Section titled “A script calls a stored graph”spool call -w starts the stored graph, waits for it holding no agent,
and prints its output as JSON:
#!/usr/bin/env bashset -euout="$(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.2The 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.
A step computes the graph
Section titled “A step computes the graph”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/v1nodes: - 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.
Events connect them loosely
Section titled “Events connect them loosely”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: publishjobs: 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/v1name: on-imagenodes: - 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.
Input and output across the boundary
Section titled “Input and output across the boundary”| 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.