Shipfox
Reference

Expressions and Interpolation (CEL)

Shipfox evaluates CEL in trigger filters, if conditionals, gates, and job success, and resolves ${{ }} templates in commands, env, prompts, and outputs from staged run context.

Shipfox uses CEL (Common Expression Language) in two roles. Predicates decide an outcome: does this event fire the trigger, does this job or step run, did a gated step succeed. Templates produce strings: the ${{ }} syntax fills run context into prompts, commands, env values, names, and outputs. Both roles use the same read-only engine: expressions compute over the data in scope, and they cannot reach the database, the network, or the filesystem.

One rule separates the two roles. Templates may read your configuration, but predicates read run state only. A template can inject ${{ vars.API_URL }} into a prompt, and a run step can bind ${{ secrets.NPM_TOKEN }} into its env. A predicate (filter, if, gate.success, job success) can see the event, step results, and outputs, but never a variable or a secret. This keeps decisions reproducible from run state and keeps secret values out of every evaluation path.

The CEL engine

Expressions support CEL's standard value types (integers, strings, booleans, lists, maps), its standard operators (==, !=, <, >, &&, ||, !, in, arithmetic) and macros (.map(), .filter(), .all(), .exists()), plus string helpers like .contains(), .startsWith(), and .endsWith(). There are no custom functions and no side effects.

Predicates

Trigger filters

A trigger's filter runs when the event arrives, before any run is created. In scope: event (the payload) and trigger (source and event). A false result skips the event. An expression error fails closed: the trigger does not fire. The same filter shape applies to a listening job's on and until entries.

This trigger fragment belongs under an existing workflow's triggers map:

triggers:
  on_main_push:
    source: github_acme
    event: push
    filter: event.ref == "refs/heads/main"

Conditionals: if

Jobs and steps accept an if: field holding exactly one ${{ }}-wrapped CEL boolean. When it is false, the job or step is marked skipped, not failed. An evaluation error also skips, recorded with a distinct reason.

This job fragment belongs under an existing workflow's jobs map:

jobs:
  deploy:
    if: ${{ event.ref == "refs/heads/main" }}
    steps:
      - key: build
        run: ./build.sh
        outputs:
          artifact_count: number
      - run: ./deploy.sh
        if: ${{ steps.build.outputs.artifact_count > 0 }}

Gate success: gate.success

Evaluated the moment a gated step finishes. In scope: step.exit_code (an integer), step.status (a string), and step.outputs (the step's declared outputs). See Feedback loops for the full gate model.

This gate fragment belongs on a run or agent step:

gate:
  success: step.exit_code == 0

Job success: success

A job's optional success expression decides whether the job succeeded once all its executions settle. In scope is executions, a list where each element carries index, name, status, events, outputs, and timing fields. The default succeeds when no execution failed. It also succeeds when a listening job resolves with no executions:

This job fragment belongs under an existing workflow's jobs map:

jobs:
  build:
    success: '!executions.exists(e, e.status == "failed")'
    steps:
      - run: ./maybe-flaky.sh

Add this success field to a job to require at least one execution:

success: 'executions.size() > 0 && executions.all(e, e.status == "succeeded")'

Templates: ${{ }}

Interpolation fills run context into string values. It works in:

  • run commands and env values (run steps)
  • agent prompt, model, and provider
  • job runner, name, and outputs
  • step name and gate on_failure.feedback

To write a literal ${{, escape it as $${{.

Resolution is staged. Each reference resolves at the moment its data exists: event, inputs, and vars when the run is created, needs and jobs when the job starts, steps.* and step.* when the step is dispatched, and gate feedback when the gate fails. You just write the reference. Shipfox fills it as late as needed. A reference that cannot resolve fails the run or step with a typed error instead of passing empty text through.

This job fragment belongs under an existing workflow's jobs map:

jobs:
  build:
    env:
      SOURCE: "${{ trigger.source }}"
    steps:
      - run: echo "Building for ${{ trigger.event }}"
      - prompt: "Investigate the event that triggered this run: ${{ event.title }}"

Context available

RootHoldsTrustAvailable from
runid, name, definition_id, project_id, workspace_id, created_atTrustedRun creation
triggersource, eventTrustedRun creation
eventThe trigger's raw event payload (open shape, e.g. event.ref, event.title)UntrustedRun creation
inputsValues from the trigger's with block (open shape)UntrustedRun creation
varsWorkspace/project variables, literal keys onlyTrustedRun creation
needsList of direct dependency jobs with their status and outputsUntrustedJob start
jobsNamed upstream jobs with their status and outputsUntrustedJob start
executionCurrent job execution metadata and its listening-job eventsTrusted (event data untrusted)Job execution
steps.<key>Earlier steps' status, exit_code, outputs, attemptsTrusted (outputs untrusted)Step dispatch
stepattempt, is_retry, restart.from, restart.feedbackTrustedStep dispatch
secretsWorkspace/project secrets, literal keys onlyRunner-onlyRun-step run and env values only

Untrusted context can't select infrastructure. event, inputs, execution.events, and outputs carry data from outside your control. They may flow into env and prompt values, but not directly into run commands or into an agent's model, provider, or thinking fields. Bind outside data to env before using it in a shell command.

Secrets and variables

${{ vars.KEY }} resolves server-side when the run is created and works in every template site. ${{ secrets.KEY }} never resolves on the server: it is allowed only in a run step's run command or env values, and the runner pulls the value just before the step executes. Neither works in predicates. The full model lives in Secrets and variables.

Inside run commands

Trusted values can be interpolated into a run command. Untrusted values from events, inputs, listening events, or outputs are rejected there. Bind an untrusted value to env, then quote the shell variable in the command. This keeps outside data separate from shell code.

Outputs

Steps declare typed outputs and later steps or jobs read them through steps.<key>.outputs and needs/jobs. The authored shape and the runtime mechanics live in Step outputs. The mental model lives in Context and templating.

Shipped vs. roadmap

FeatureStatus
Trigger filter evaluation✅ Shipped
if conditionals on jobs and steps✅ Shipped
gate.success (exit code, status, outputs) and job success✅ Shipped
${{ }} templates in commands, env, prompts, names, outputs, feedback✅ Shipped
Step and job outputs✅ Shipped
${{ vars.* }} and ${{ secrets.* }}✅ Shipped
loop, matrix, branch🔜 Roadmap
Was this page helpful?
Edit this page on GitHub

On this page