Shipfox
Understand

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.

StepPerformsRuns on
Run stepA shell command in the job's checkout.The runner.
Agent stepA goal that an agent pursues in the checkout.The runner.
Tool stepOne 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.

.shipfox/workflows/release.yml
# 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 . helm

In 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:

.shipfox/workflows/check-and-review.yml
# 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 needs did 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_afterA step runs whenA job runs when
success (default)No earlier step in the job failed.Every job in needs succeeded.
failureAn earlier step in the job failed.At least one job in needs failed.
alwaysWhatever 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:

  1. Shipfox checks run_after. If it does not pass, Shipfox skips the step or job.
  2. 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:

.shipfox/workflows/fix-and-report.yml
# 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:

  • push has an if and no run_after. Shipfox skips it when test fails.
  • summary runs when test passes and when test fails.
  • report_failure runs only when the fix job fails.

Which value to use

  • Use failure for work that is useful only after a failure, such as a failure report.
  • Use always for work that must run in both cases, such as a summary or a cleanup. Its if can 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: success runs.
  • A later step with run_after: failure does not run because of this step.
  • execution.failed stays false.
  • 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: true starts a step and moves on to the next step. The step needs a key, because wait and cancel name steps by key.
  • wait blocks until the background steps it names end. wait_all blocks until every background step started earlier ends.
  • cancel stops the background steps it names, without waiting for them.
  • parallel runs 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:

.shipfox/workflows/test-with-database.yml
# 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: db

In 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 parallel block 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 key when 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_error never makes a wait, a run_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, then SIGKILL after 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 parallel block appear as a group. Its last row is the wait, labeled Wait for parallel steps.
  • A background step carries a background marker.
  • Each wait shows how long it blocked the job.
  • Several steps can be running, each with its own live log.
  • A step whose failure continue_on_error ignored shows failure ignored.

Concurrent steps or separate jobs

The work needsUse
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.

Was this page helpful?
Edit this page on GitHub

On this page