Jobs and steps
Understand how the split into jobs affects shared files, parallel work, runner placement, and results, and when steps of one job run at the same time.
A job is the unit that Shipfox places on a runner. A step is one action inside that job. A step is a run step, an agent step, or a tool step. Steps start in the order they appear in the file.
The main design choice is where one job ends and the next begins. That choice decides which steps share a checkout, what can run at the same time, and which results must pass from one job to another.
Three kinds of step
Each kind of step performs a different kind of work.
| Step | Performs | Runs on |
|---|---|---|
| Run step | A shell command in the job's checkout. | The runner. |
| Agent step | A goal that an agent pursues in the checkout. | The runner. |
| Tool step | One integration operation with inputs the workflow supplies. | The Shipfox API, through an integration connection. |
A tool step has no working tree. It can't read files, set env, or use
secrets. Its inputs come from run data, and its result becomes step outputs
for later steps. Shipfox makes the call, so the provider credential never
reaches the runner.
The job still holds its runner while a tool step runs. The runner waits for the call to finish and then continues with the next step. A job made only of tool steps still needs a runner. Integrations, integration connections, and tools explains when a tool step fits better than an agent with tools.
What lands at the job root
Every step starts in the job root, shown as .. By default the root holds a
checkout of the project that owns the workflow. Each job gets its own copy.
A first step that is a checkout can replace it. When that step sets no path,
or sets . or ./, its repository lands at the root instead, and Shipfox skips
the project checkout. When it sets any other path, such as path: helm, the
repository goes into that folder and the project checkout stays at the root.
Set checkout: false on the job to skip the project checkout and start with an
empty root. A later checkout step with no path, or with . or ./, can then
fill it.
Only one checkout owns the root. Every other checkout step must set path, and
Shipfox rejects the workflow when one doesn't.
# yaml-language-server: $schema=https://www.shipfox.io/docs/workflow.schema.json
name: Release
runner: shipfox
triggers:
manual:
source: manual
jobs:
release:
steps:
- checkout:
repository: acme/helm
path: helm
- run: ls . helmIn this job, . holds the project and helm holds acme/helm.
When to keep steps in one job
Steps in one job use the same checkout. By default, a step starts after the step before it ends. A later step can read files that an earlier step changed. It can also use packages that an earlier step installed.
Steps don't share one long-lived shell process. A shell variable set in one run step doesn't reach the next. Save a file in the checkout when the next step needs it. Publish an output when the value must become workflow data.
One job is usually the right choice when steps must:
- Edit and check the same files.
- Reuse an expensive local setup.
- Repeat together in a feedback loop.
- Use the same runner capabilities and repository permission.
The tradeoff is less parallelism. By default, one slow step holds the job's runner and delays every later step. Steps that run at the same time remove that delay for work that doesn't touch the same files.
When to split work into jobs
Each job gets a fresh checkout. It can use a different runner or repository permission. Jobs without dependencies can start at the same time when capacity is free.
Separate jobs fit work that must:
- Run in parallel in separate checkouts.
- Fail or skip on its own.
- Use a different execution environment.
- Expose a small, named result to later work.
The cost is extra setup. Two jobs that both need Node.js packages each install them again. Their files, process environment, and logs stay separate.
This complete workflow shows the split:
# yaml-language-server: $schema=https://www.shipfox.io/docs/workflow.schema.json
name: Check and review a change
runner: shipfox
triggers:
manual:
source: manual
jobs:
lint:
steps:
- run: npm ci && npm run lint
test:
steps:
- run: npm ci && npm test
review:
needs: [lint, test]
steps:
- prompt: Review the repository for likely bugs and missing tests.lint and test can run in parallel. Each installs its own dependencies.
review waits for both, but receives neither job's files nor logs. It reviews
its own fresh checkout.
The shipfox label selects the default hosted runner. The Hosted runners
reference lists the other sizes. The job fields
reference defines the dependency syntax.
When a step or job runs
A failure stops the work that comes after it. This is the default:
- Shipfox skips a step when an earlier step in the same job failed.
- Shipfox skips a job when a job in its
needsdid not succeed.
The default protects writes. For example, a step that pushes a branch does not run when the tests before it failed.
Set run_after on a step or a job to change the default.
run_after | A step runs when | A job runs when |
|---|---|---|
success (default) | No earlier step in the job failed. | Every job in needs succeeded. |
failure | An earlier step in the job failed. | At least one job in needs failed. |
always | Whatever earlier steps did. | Whatever the status of needs. |
A failed background step counts as a
failure only from the wait that joins it.
A job always waits until every job in its needs has finished. With
run_after: failure, a job looks only at the jobs in its own needs. It does
not look at the jobs that those jobs need.
How if works with run_after
An if does not replace run_after. Shipfox checks the two fields in this
order:
- Shipfox checks
run_after. If it does not pass, Shipfox skips the step or job. - Shipfox checks the
if. If it is false, Shipfox skips the step or job.
A step or job runs only when both checks pass. A step with an if and no
run_after uses the default, so Shipfox still skips it after an earlier
failure.
This workflow shows the default, always, and failure:
# yaml-language-server: $schema=https://www.shipfox.io/docs/workflow.schema.json
name: Fix and report
runner: shipfox
triggers:
manual:
source: manual
with:
push: "true"
jobs:
fix:
checkout:
permissions:
contents: write
steps:
- key: test
run: npm ci && npm test
# Skipped when the test step failed, even though it has an if.
- key: push
if: ${{ inputs.push == "true" }}
run: git push origin HEAD
# Runs whether the earlier steps passed or failed.
- key: summary
run_after: always
run: echo "Tests finished."
report_failure:
needs: [fix]
run_after: failure
steps:
- run: echo "The fix job failed."In this workflow:
pushhas anifand norun_after. Shipfox skips it whentestfails.summaryruns whentestpasses and whentestfails.report_failureruns only when thefixjob fails.
Which value to use
- Use
failurefor work that is useful only after a failure, such as a failure report. - Use
alwaysfor work that must run in both cases, such as a summary or a cleanup. Itsifcan read a status to do different work in each case.
Let a step fail without failing the job
Set continue_on_error: true on a step whose failure must not stop the job.
The step still ends failed, and the run page marks the failure as ignored.
The rest of the job acts as if the step succeeded:
- A later step with
run_after: successruns. - A later step with
run_after: failuredoes not run because of this step. execution.failedstaysfalse.- The job can succeed with this step failed.
steps.<key>.status still reads failed, so a later step can check it:
steps:
- key: lint
continue_on_error: true
run: npm run lint
- run: npm test
- if: ${{ steps.lint.status == "failed" }}
run: echo "Lint failed. The job goes on."In this job, npm test runs when lint fails. The last step runs only when
lint failed. The job succeeds when npm test passes.
A step with a feedback loop restarts first.
continue_on_error applies only to its last failure, after its restart limit
is used up.
continue_on_error covers a failure of the step's own work. It does not cover
a step that cannot start, such as an if that cannot be evaluated or an input
that cannot be filled. It also does not cover a job that times out. Those
failures still fail the job.
A step that uses a composite action cannot set continue_on_error. Set it on
the steps inside the action. wait, wait_all, and cancel steps cannot set
it either. Inside a parallel block, set it on each step that may fail.
Read outputs after a failure
A skipped step sets no outputs. An output without a default then has no
value. A step or job that runs after a failure can read such an output. Shipfox
then cannot evaluate the condition, and it skips the step or job. Check the
output with has() first:
if: ${{ has(steps.test.outputs.report) }}Find out why Shipfox skipped a step or job
The run page tells you when run_after skipped a step or a job. It also shows
the check that did not pass.
Workflows that Shipfox synced before run_after existed keep the old
behavior: an if replaces the default. The new behavior starts the next time
the workflow file changes.
The step fields and job fields references define the field.
Run steps of one job at the same time
Some steps don't need the step before them to end. A test database can start while dependencies install. Three agents can review the same code at once. Shipfox can run such steps at the same time, on the job's runner and in its checkout. The fields follow GitHub Actions.
background: truestarts a step and moves on to the next step. The step needs akey, becausewaitandcancelname steps by key.waitblocks until the background steps it names end.wait_allblocks until every background step started earlier ends.cancelstops the background steps it names, without waiting for them.parallelruns a list of steps at the same time, then waits for them all.
Run, action, agent, and tool steps can run in the background. A checkout step
can't. Shipfox checks the if and run_after of a background step when the
step would start.
This complete workflow starts a database, prepares the tests at the same time, and stops the database at the end:
# yaml-language-server: $schema=https://www.shipfox.io/docs/workflow.schema.json
name: Test with a database
runner: shipfox
triggers:
manual:
source: manual
jobs:
test:
steps:
# Starts Postgres and moves on to the next step.
- key: db
background: true
run: docker run --rm --name pg -p 5432:5432 -e POSTGRES_PASSWORD=pg postgres:17
# Runs both steps at the same time, then waits for both.
- parallel:
- run: npm ci
- run: docker pull ghcr.io/acme/fixtures:latest
- run: npx wait-on tcp:127.0.0.1:5432
- run: npm test
# Stops Postgres. Without it, the job waits for the server.
- cancel: dbIn this job, db keeps running while every later step runs. A step that isn't
in the background still blocks the next step. Background steps keep running
while it does.
A parallel block is shorthand. It marks each step in the list as a background
step, then adds a wait that names exactly those steps. It doesn't wait for
other background steps that were already running, such as db here.
Concurrent steps share everything
Steps that run at the same time share the job's working tree, its environment, and its job container. Shipfox does not isolate them. When two steps write the same file at the same time, the result depends on timing. Shipfox doesn't detect or prevent it.
Concurrent steps fit work that doesn't collide:
- Agents that only read the code.
- Setup steps that write to different paths.
Run steps that edit the same files one after the other, or in separate jobs.
Concurrent steps also share the runner's CPU and memory. Each step keeps its own log, but the job's log budget is shared, so concurrent steps use it up faster. Shipfox also limits how many steps of one job run at once. A step above the limit waits, in order, for a free slot. Limits gives both numbers.
When results become readable
A background step's results become readable only after a wait that joins it.
These results are its steps.<key>.outputs, its steps.<key>.status, and the
variables it wrote to $SHIPFOX_ENV or $SHIPFOX_PATH.
- Steps that run at the same time never see each other's results.
- A step inside a
parallelblock sees every step before the block. It sees nothing from the other steps in the block. - After the block, every step of the block is readable. Give a step a
keywhen a later step must read it. - When several joined steps write the same variable, the step that comes later in the file wins. The order in which the steps end doesn't matter.
Sync rejects an expression that reads a step before its results are readable.
When a background step fails
A failed background step doesn't fail the job at once. The failure shows at the
wait that joins it. That wait fails, and the job fails from there. Later
steps follow their run_after.
- Steps that run next to the failed step run to the end.
- A failed step that isn't in the background doesn't stop the background steps.
- A step with
continue_on_errornever makes await, arun_after, or the job fail.
A failed step keeps only the outputs it wrote before it failed. Give an output a
default when a later step reads it after a possible failure. The parallel
review recipe shows this with three
agent reviewers.
When a background step is cancelled
cancel stops a step in the way that fits its kind:
- A run or action step gets
SIGTERM, thenSIGKILLafter a short period. - An agent step stops its turn.
- A tool step that already called its provider is not undone. Shipfox ignores its result.
The step ends cancelled, which is not a failure. Its outputs are never
readable. Cancelling a step that already ended does nothing. Cancelling a step
that hasn't started yet marks it cancelled, and it never starts.
cancel doesn't wait for the step to stop. These wait until the process exits:
- A checkout step.
- The post of an action.
- A step that reuses the cancelled step's agent session.
- The end of the job.
The run page shows Cancelling until the process exits. A cancelled action step still gets its post. The post sees the state that the action saved before it stopped.
When the steps run out
After the last step, Shipfox waits for every background step that is still
running, as if the job ended with wait_all. The run page shows this as a
Wait for background steps row. A failure there fails the job. Action posts
run after this wait, including the post of a background action.
A background step that never exits, such as a server, holds the job until its
execution_timeout. End such a step with cancel.
Follow concurrent steps on the run page
- The steps of a
parallelblock appear as a group. Its last row is the wait, labeled Wait for parallel steps. - A background step carries a background marker.
- Each
waitshows how long it blocked the job. - Several steps can be running, each with its own live log.
- A step whose failure
continue_on_errorignored shows failure ignored.
Concurrent steps or separate jobs
| The work needs | Use |
|---|---|
| One checkout and one dependency install. | Concurrent steps in one job. |
| Separate files or a separate environment. | Separate jobs. |
| Another runner, or to fail or skip on its own. | Separate jobs. |
| To edit the same files. | Steps that run one after the other. |
The concurrent step rules list what sync checks.
How results move between jobs
A job output passes a small value to later jobs. It fits a version, a decision, an identifier, or a URL.
An artifact is a file or a directory. One job's checkout isn't available to another job. Put large reports and build products in an external store. Pass only their location as an output.
This explicit hand-off means a later job can't depend on a file that another job left behind by accident. It also makes reruns and runner placement easier to predict.
A named agent session is a third way to pass context. It gives one agent's conversation to a later agent step in the same run. It doesn't pass files either.
A job also chooses its runner and its access
A job selects its runner labels as a whole. Split work when one part needs a private network, special hardware, or another operating system. Keep it together when every step needs the same environment.
Repository checkout permission also applies to the whole job. Each agent step selects its own integration tools, and each tool step names one operation. Both are separate from Git access. This matters when an agent reads code but only one later step must write to an external system.
Runners and execution environments explains placement. Agent access shows the separate repository and integration controls. Call an integration tool shows a tool step.
Retries and later events stay inside the job
A feedback loop repeats steps inside one job execution, so the repeated work keeps the same checkout. A listening job creates a new job execution for each later event or event batch. It can run its steps several times in one run.
Choose the job boundary before you add either kind of repetition. A job that mixes unrelated writes makes retries and later events harder to handle safely.