Expressions (CEL)
qp uses CEL (Common Expression Language) for runtime conditions. CEL expressions appear in two places: task when clauses that gate whether a task runs, and when()/switch() nodes inside DAG run expressions.
Where Expressions Are Used
Task when clause
A when field on a task is a CEL expression that must evaluate to true for the task to execute. If false, the task is skipped silently.
tasks:
deploy:
desc: Deploy only from main in prod
cmd: ./scripts/deploy.sh
when: branch() == "main" && profile() == "prod"qp deploy # skipped unless on main with prod profile
qp deploy --profile prod # runs if also on main branchRun expression conditions
Inside a run DAG, when() and switch() accept CEL expressions as their condition argument:
tasks:
release:
run: when(branch() == "main", publish, dry-run)
deploy:
run: >
switch(env("TARGET"),
"api": deploy-api,
"web": deploy-web
)See DAGs and Run Expressions for the full DAG syntax.
Built-in Functions
| Function | Returns | Description |
|---|---|---|
branch() |
string |
Current git branch name. Returns "" if not in a git repo. |
tag() |
string |
Git tag on HEAD, or "" if HEAD is not tagged. |
profile() |
string |
Active profile name, or "" if none. |
env("NAME") |
string |
Value of environment variable NAME, or "" if unset. |
param("name") |
string |
Value of task parameter name, or "" if not provided. |
has_param("name") |
bool |
true if parameter name was passed to the task. |
file_exists("path") |
bool |
true if the file exists. Relative paths resolve from the repo root. |
All functions return empty strings (not errors) when the underlying value is absent, so expressions like tag() != "" are safe without a guard.
Built-in Variables
In addition to the functions above, several variables are available directly in expressions:
| Variable | Type | Description |
|---|---|---|
os |
string |
Operating system — "darwin", "linux", or "windows" (from Go’s runtime.GOOS). |
branch |
string |
Same value as branch(). |
tag |
string |
Same value as tag(). |
profile |
string |
Same value as profile(). |
env |
map[string]string |
Full environment variable map. Access with env["NAME"]. |
params |
map[string]string |
All task parameters. Access with params["name"]. |
vars |
map[string]string |
Configuration variables from qp.yaml. Access with vars["key"] or vars.key. |
var |
map[string]string |
Alias for vars. |
repo_root |
string |
Absolute path to the repository root. |
Functions and variables overlap intentionally — use whichever reads more clearly. branch() == "main" and branch == "main" are equivalent.
Operators
CEL supports the standard set of operators:
Comparison
== != < > <= >=
Logical
&& || !
Arithmetic
+ - * / %
String concatenation
"hello" + " " + "world"
Map/list indexing
env["CI"] // bracket notation
vars.region // dot notation (equivalent to vars["region"])
Examples
Gate on branch
tasks:
deploy:
desc: Only deploy from main
cmd: ./scripts/deploy.sh
when: branch() == "main"Gate on OS
tasks:
install-deps:
desc: macOS-only dependency install
cmd: brew install protobuf
when: os == "darwin"Combine conditions
tasks:
publish:
desc: Publish tagged releases from main
cmd: ./scripts/publish.sh
when: branch() == "main" && tag() != ""Check environment variables
tasks:
test:
desc: Run tests with coverage in CI
cmd: go test -coverprofile=coverage.out ./...
when: env("CI") == "true"Use parameters
tasks:
migrate:
desc: Run migrations only when explicitly requested
params:
run_migrations:
desc: Set to true to run migrations
cmd: ./scripts/migrate.sh
when: has_param("run_migrations") && param("run_migrations") == "true"Access configuration variables
vars:
region: us-east-1
tasks:
deploy-eu:
desc: Deploy only when targeting EU
cmd: ./scripts/deploy-eu.sh
when: vars.region == "eu-west-1"File existence guard
tasks:
generate:
desc: Generate code only if spec file exists
cmd: go generate ./...
when: file_exists("api/openapi.yaml")
build:
desc: Build only if dist doesn't already exist
cmd: go build -o dist/app ./cmd/app
when: "!file_exists(\"dist/app\")"Note the quoting — YAML requires quotes around expressions that start with ! to avoid being parsed as a YAML tag.
Map indexing
tasks:
notify:
desc: Notify when running in CI
cmd: ./scripts/notify.sh
when: env["CI"] == "true"env("CI") and env["CI"] are equivalent. The function form is generally cleaner; bracket notation is useful when you already have the full map.
Functions vs Variables
Both branch() (function) and branch (variable) return the same value. The same applies to tag(), profile(), and env().
Use the function form for consistency and readability. Use the variable/map form when composing dynamic lookups or when dot-notation is cleaner:
# Function form — cleaner for simple checks
when: branch() == "main"
# Variable form — useful for map access
when: vars.deploy_target == "prod"
# Both work for env
when: env("CI") == "true"
when: env["CI"] == "true"CEL Language Reference
qp uses cel-go, the Go implementation of CEL. Expressions support the full CEL specification including:
- Boolean, string, integer, double, and bytes literals
- Ternary:
condition ? value_if_true : value_if_false - The
inoperator for list/map membership - String methods like
.contains(),.startsWith(),.endsWith(),.matches(),.size() - List operations like
.size(),.exists(),.all(),.filter(),.map()
For the full language spec, see the CEL language definition.
Next Steps
- DAGs and Run Expressions — use CEL in
when()andswitch()DAG nodes - Parameters — define parameters that feed into
param()andhas_param() - Profiles — control what
profile()returns - Variables — define
varsaccessible in expressions