Composing tasks across files
When a single zorb.yml starts feeling crowded — typically once a project has its own deployment plumbing — split the workflow across files and use uses: to call between them. zorb composes tasks call-style; there's no DAG, no needs:, just function calls.
The shape
A project with a root workflow for app tasks and an ops/ workflow for deployment plumbing:
.
├── zorb.yml # build, test, release
├── ops/
│ └── zorb.yml # plan, apply, rollback
└── infra/
└── zorb.yml # tf plan, tf applyRoot workflow:
# zorb.yml
tasks:
build:
description: Build the project
steps:
- run: npm run build
release:
description: Build and deploy to production
steps:
- uses: ./zorb.build # same-file ref
- uses: ./ops/zorb.apply # cross-file ref
with:
environment: productionOps workflow:
# ops/zorb.yml
tasks:
plan:
description: Print the changes a deploy would make
inputs:
environment:
type: string
required: true
steps:
- uses: ../infra/zorb.plan
with:
workspace: ${{ inputs.environment }}
apply:
description: Roll a build out to an environment
inputs:
environment:
type: string
required: true
steps:
- uses: ./zorb.plan # task in this same file
with:
environment: ${{ inputs.environment }}
- uses: ../infra/zorb.apply
with:
workspace: ${{ inputs.environment }}
auto-approve: trueRun it from the project root:
zorb run release # build + apply production
zorb --file ops/zorb.yml run plan --with environment=stagingHow the resolver finds the other file
uses: ./[dir/]zorb.<task> is the cross-file form. zorb spots it by the zorb. basename: anything else is either an action file or an NPM action.
- The path is relative to the calling workflow's directory, not the CLI's
cwd. Souses: ../infra/zorb.planfromops/zorb.ymlreachesinfra/zorb.ymlcorrectly even when you ranzorb run releasefrom the project root. - The taskname after
zorb.cannot contain dots —zorb.deploy.stagingis invalid (it's parsed as a workflow ref to a malformed task name). - Cycles are detected at runtime and error.
AcallingBcallingAproduces a clear cycle message before any step runs.
See Workflow format → Resolution for the precise resolution order.
What the callee sees
A cross-file task is not a subprocess — it's invoked by zorb's own runner. But it's still isolated:
- Inputs are explicit. The callee only sees the keys you pass via
with:. The caller'sinputsdo not bleed through. If the deploy task needs to know which environment, you passwith: { environment: … }. - Env layers reset. The callee's
env:stack starts from the workflow'senv:, just like an outerzorb runwould. Inline CLI env (--env-file,-e) flows through because it's set at the process level. - Secrets table is shared. Pre-task secrets loaded by the outer workflow remain visible to the inner one.
So a cross-file call is closer to "function call with explicit arguments" than "subprocess." Outputs from the callee's steps stay inside the callee — they don't surface in the caller.
When to split
Rough heuristics:
- Same domain, same lifecycle → keep it in one file.
build,test,lintbelong together. - Different lifecycle or owner → split. Application code vs. infrastructure rollout. Local dev vs. CI-only tasks. Splitting makes the boundary explicit and gives each side its own
env:,defaults:, andsecrets:block. - Different blast radius → split. The release workflow that pushes to npm shouldn't share a directory with the smoke-test workflow.
If splitting feels premature, it probably is. One zorb.yml with eight tasks is fine.
Calling a sibling task from the same file
The same ./zorb.<task> syntax works for tasks in the calling file:
tasks:
build:
steps:
- run: npm run build
release:
steps:
- uses: ./zorb.build # this same file
- run: ./scripts/push.shUseful when a task wants to be callable on its own and used as a step elsewhere. The alternative — copy-pasting steps — drifts the moment one side changes.
See also
- Multi-environment deploy — drives a cross-file deploy task with
--with. - Concepts → Three kinds of step — where workflow refs sit in the step model.
- Workflow format → Resolution — the exact rule the resolver applies.