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 == 0Job 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.shAdd 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:
runcommands andenvvalues (run steps)- agent
prompt,model, andprovider - job
runner,name, andoutputs - step
nameand gateon_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
| Root | Holds | Trust | Available from |
|---|---|---|---|
run | id, name, definition_id, project_id, workspace_id, created_at | Trusted | Run creation |
trigger | source, event | Trusted | Run creation |
event | The trigger's raw event payload (open shape, e.g. event.ref, event.title) | Untrusted | Run creation |
inputs | Values from the trigger's with block (open shape) | Untrusted | Run creation |
vars | Workspace/project variables, literal keys only | Trusted | Run creation |
needs | List of direct dependency jobs with their status and outputs | Untrusted | Job start |
jobs | Named upstream jobs with their status and outputs | Untrusted | Job start |
execution | Current job execution metadata and its listening-job events | Trusted (event data untrusted) | Job execution |
steps.<key> | Earlier steps' status, exit_code, outputs, attempts | Trusted (outputs untrusted) | Step dispatch |
step | attempt, is_retry, restart.from, restart.feedback | Trusted | Step dispatch |
secrets | Workspace/project secrets, literal keys only | Runner-only | Run-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
| Feature | Status |
|---|---|
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 |
Related pages
Workflow schema
The full YAML schema these expressions live in.
Context and templating
The mental model: what data flows through a run, and why the rules exist.
Workflow Schema
Complete reference for the Shipfox workflow YAML schema: top-level fields, triggers, jobs, run and agent steps, gates, environment variables, and validation rules.
Trigger Sources
Every Shipfox trigger source and its events at a glance: manual fire, Cron schedules, GitHub, Sentry, and custom webhooks.