Parser pipeline
- Normalize and parse YAML. Workflow keys are deserialized into typed models. YAML’s boolean spelling of
onis normalized to the trigger key, empty workflows are rejected, event names are validated, and expression syntax and function calls are checked before a run is admitted. - Project the event. Trigger matching considers the event name, activity
types, branch or tag filters, path filters, workflow-dispatch inputs, and upstream workflow names forworkflow_run. A mismatch reports the rejected axis rather than silently creating an empty run. - Resolve reusable workflows. Local and remote reusable workflow files are loaded into the call tree. The expansion guard allows at most 10 connected workflow levels (the top-level caller plus nine nested levels) and 50 unique reusable workflow references in one run tree.
- Expand jobs and matrices. Jobs become
JobPlanrecords. Matrix axes produce stable cells;includecan add or augment cells andexcluderemoves matching cells. A matrix is capped at 256 combinations.fail-fastandmax-parallelremain part of the plan for control-plane scheduling. - Build and validate the DAG.
needs:references must name an existing job, and cycles are rejected. A dependency closure can be selected for a single job. Jobs whose matrix or reusable-workflow inputs depend onneedsare stored as deferred expansion nodes and expanded when their dependencies settle. - Build runner messages. The job builder converts each plan into the encrypted runner message: steps, environment, contexts, token permissions, action metadata, timeout, timeline references, and the job’s dependency outputs.
preloop plan to inspect the expanded job DAG without executing it.
What is evaluated where
The server leaves raw step
if conditions in the runner message because the runner has the completed step context. Expressions that mention needs or github.workspace are deliberately deferred instead of being resolved to an empty string. The runner supplies runtime contexts such as runner, steps, and the final workspace while preserving the server-provided github, matrix, strategy, job, inputs, env, vars, and secrets roots.
Expression language
Expressions use${{ ... }} delimiters. The evaluator supports:
null, booleans, numbers, and single-quoted strings;- dotted paths such as
github.refand bracket access such asmatrix['target']; - wildcard member access (
*) over arrays and objects; - unary
!, logical&&and||, and comparisons==,!=,<,<=,>, and>=; and - the built-ins
always(),success(),failure(),cancelled(),contains(),startsWith(),endsWith(),format(),fromJSON(),toJSON(),join(),hashFiles(), andcase().
success() gate used by Actions. success() and failure() can take dependency names in job conditions; step-level validation narrows them to the runner’s zero-argument form.
hashFiles() is evaluated relative to the workspace and is bounded by file-count, traversal, and input-size limits. format() and expression evaluation also have output and temporary-value budgets. Expressions deeper than 128 AST levels are rejected before evaluation.
Unknown functions, invalid arity, malformed JSON passed to fromJSON(), disallowed hashFiles() paths, and invalid syntax fail validation or evaluation rather than being silently treated as a false condition.
Contexts and secrets
The engine builds contexts forgithub, env, vars, secrets, needs, steps, matrix, strategy, job, runner, and inputs. Secret references are collected from the workflow tree so the control plane can inject only the names needed by the job. Values come from Preloop’s local secret store; the engine does not read GitHub’s write-only secret store. Engine-provided values such as GITHUB_TOKEN, runtime URLs/tokens, cache URLs, and Results URLs are minted at the job boundary and are not reported as missing user secrets.
A workflow can use deferred matrix values from fromJSON(needs.<job>.outputs.<name>); those cells are expanded only after the producing job’s outputs are recorded. This keeps the durable graph and runner message aligned with the data that actually exists at promotion time.