Skip to content

CEL reference

zenflow uses CEL (Common Expression Language) for two fields:

  • step.condition - a boolean expression. False means skip the step.
  • loop.until - a boolean expression. True means stop the loop.
  • loop.forEach - an expression returning an array (when not given as a static array literal).

CEL is implemented via cel-go. zenflow registers the variables listed below; no extra functions are exposed beyond CEL's standard library. CPU cost is bounded at evaluation time (CostLimit(10000)), so expressions complete in microseconds to a few milliseconds.

This page is a quick reference. For the full CEL language, see the spec.

Available variables

The variable set depends on where the expression appears.

In step.condition

Evaluated after all dependsOn finish, before this step starts.

VariableTypeDescription
steps.<id>.contentstringConcatenated text from a completed dependency step.
steps.<id>.statusstringOne of completed, failed, skipped, cancelled.
steps.<id>.resultobjectStructured result of a completed dependency step.

content and result (without the steps. prefix) are not in scope here - the step has not run yet.

In loop.until and loop.forEach

Evaluated after each iteration (for until) or once at loop entry (for forEach). All variables below capture the just-completed iteration.

VariableScopeTypeDescription
iterationrepeat-untilintZero-based iteration number.
indexforEachintZero-based iteration index.
itemforEachdynCurrent element from the array.
contentsingle-step repeat-untilstringText content of the step.
resultsingle-step repeat-untilobjectStructured result of the step.
statussingle-step repeat-untilstringStatus of the step.
steps.<id>.contentmulti-step loopstringInner step content.
steps.<id>.statusmulti-step loopstringInner step status.
steps.<id>.resultmulti-step loopobjectInner step structured result.

In multi-step loops (loop.steps present), address inner steps via steps.<inner-id> because the bare content/result are reserved for single-step bodies.

Evaluation rules

  • condition and until must evaluate to a boolean. A non-boolean result is an error and aborts the step or loop.
  • forEach (when a string) must evaluate to an array. A non-array result fails the loop.
  • An empty string is rejected for condition (minLength: 1 in the schema). The same applies to until.
  • CEL evaluation has no context.Context; it cannot be cancelled mid-expression. The CostLimit(10000) ceiling prevents pathological inputs.

Common patterns

Status checks

yaml
condition: "steps.tests.status == 'completed'"
condition: "steps.scan.status != 'skipped'"

Content substring match

yaml
condition: "steps.review.content.contains('LGTM')"
condition: "steps.build.content.contains('error')"

Structured result fields

yaml
condition: "steps.scan.result.severity == 'critical'"
condition: "steps.classify.result.priority >= 3"

Boolean composition

yaml
condition: "steps.tests.status == 'completed' && steps.tests.content.contains('PASS')"
condition: "steps.audit.result.passed || steps.audit.result.warnings_only"

List membership and existence quantifiers

yaml
condition: "steps.scan.result.findings.exists(f, f.severity == 'critical')"
condition: "'auth' in steps.design.result.modules"

Numeric comparison and arithmetic

yaml
until: "result.score >= 95"
until: "iteration >= 3 && result.improvements < 2"

forEach over a structured array

yaml
loop:
  forEach: "steps.list-services.result.services"

forEach over a filtered subset

yaml
loop:
  forEach: "steps.scan.result.findings.filter(f, f.severity == 'critical')"

filter and exists are part of the CEL standard library; they take a lambda (varname, predicate) form.

Worked example

A condition that combines status, structured result, and free-form content:

yaml
agents:
  scanner:
    description: "Scans for issues."
    resultSchema:
      type: object
      required: [findings]
      properties:
        findings:
          type: array
          items:
            type: object
            properties:
              severity: { type: string }
              file: { type: string }

steps:
  - id: scan
    agent: scanner
    instructions: "Scan the repo."

  - id: deep-audit
    dependsOn: [scan]
    condition: |
      steps.scan.status == 'completed' &&
      steps.scan.result.findings.exists(f, f.severity == 'critical')
    instructions: "Deeply audit the critical findings."

Limitations

  • No custom functions. zenflow does not register additional CEL functions beyond the standard library that cel-go ships with. If you need string parsing, regular expressions, or HTTP, do that work in an agent step and surface the result via submit_result.
  • No mutation. CEL is a pure expression language. You cannot assign, mutate, or call side-effectful operations.
  • No context propagation. Cancellation is enforced by the cost limit, not by Go's context.Context.
  • Cost limit is fixed. The evaluator caps cost at 10000 units. Pathological inputs (deeply nested comprehensions over large arrays) are rejected at runtime; keep expressions shallow.
  • String comparison is byte-wise. Use contains for substring matching; CEL does not do regex.
  • Numeric types follow CEL rules. Integer / double promotion is automatic; explicit casts (int(...), double(...)) are available when you need to be precise.
  • Result shape is implementation-determined. steps.<id>.result is the structured output produced by the step's submit_result call. If the agent has no resultSchema, result is empty - prefer content checks in that case.

For the full CEL language, including macros, comprehensions, and standard-library functions, see cel-spec/langdef.md.

Released under the Apache 2.0 License.