Skip to content

04 - CEL Prehooks

Prehooks are conditional gates on workflow steps. Before a step runs, its prehook CEL expression is evaluated; if it returns false, the step is skipped for that cycle or item.

Prehook Syntax

yaml
- id: qa_testing
  prehook:
    engine: cel                # only "cel" is supported
    when: "is_last_cycle"      # CEL expression — must evaluate to bool
    reason: "QA deferred to final cycle"   # human-readable explanation (optional)

When when evaluates to true, the step runs. When false, the step is skipped and the reason is logged.

Available Variables (Prehook Context)

These variables are available inside prehook when expressions:

Cycle & Task State

VariableTypeDescription
cycleintCurrent cycle number (1-based)
max_cyclesintTotal configured cycles
is_last_cyclebooltrue when cycle == max_cycles
task_idstringCurrent task ID
task_item_idstringCurrent item ID (empty for task-scoped steps)
task_statusstringCurrent task status
item_statusstringCurrent item status
stepstringCurrent step ID

QA & Ticket State

VariableTypeDescription
qa_file_pathstringPath to the QA file for this item
qa_exit_codeint?Exit code of the last QA step (null if not run)
qa_failedboolWhether the last QA step failed
active_ticket_countintNumber of active (unresolved) tickets
new_ticket_countintTickets created in the current cycle

Fix & Retest State

VariableTypeDescription
fix_exit_codeint?Exit code of the last fix step
fix_requiredboolWhether a fix is needed
retest_exit_codeint?Exit code of the last retest step

Build & Test State

VariableTypeDescription
build_exit_codeint?Exit code of the last build step
test_exit_codeint?Exit code of the last test step
build_errorsintNumber of build errors
test_failuresintNumber of test failures
self_test_exit_codeint?Exit code of the last self_test step
self_test_passedboolWhether the last self_test passed

Agent Output State

VariableTypeDescription
qa_confidencefloat?Agent QA confidence score (nullable; from AgentOutput.confidence)
qa_quality_scorefloat?Agent quality assessment (nullable; from AgentOutput.quality_score)
fix_confidencefloat?Fix agent confidence score (nullable)

Safety

VariableTypeDescription
self_referential_safeboolWhether this item is safe for self-referential execution

Pipeline Variables

All pipeline variables captured by previous steps are available in prehook expressions. Variables are injected with automatic type inference:

Source ValueCEL TypeExample
"42"intmy_count > 10
"3.14"doublescore >= 0.8
"true" / "false"boolfeature_enabled
'["a","b","c"]'list(string)qa_file_path in regression_target_ids
anything elsestringmy_var == "hello"

Precedence: Built-in variables (e.g. cycle, step) always take precedence over pipeline variables with the same name.

Truncated values: Variables that were spilled to disk (exceeding the 4 KB inline limit) are automatically excluded from the CEL context.

Scope merging: Both task-scoped and item-scoped pipeline variables are available. When names collide, item-scoped values take precedence.

Example: Filter by Regression Targets

yaml
# qa_doc_gen captures regression_target_ids as a JSON array
capture:
  - var: regression_target_ids
    source: stdout
    json_path: "$.regression_targets[*].id"

# qa_testing filters items using the captured list
prehook:
  engine: cel
  when: >-
    is_last_cycle
    && qa_file_path in regression_target_ids
    && self_referential_safe
  reason: "Filtered by regression targets from qa_doc_gen"

Common Patterns

Defer to Last Cycle

Run QA only on the final cycle of a multi-cycle workflow:

yaml
prehook:
  engine: cel
  when: "is_last_cycle"
  reason: "QA deferred to final cycle"

Conditional Fix

Only run fix when there are active tickets:

yaml
prehook:
  engine: cel
  when: "active_ticket_count > 0"
  reason: "No tickets to fix"

Combined Conditions

Defer QA to last cycle AND filter by safe files:

yaml
prehook:
  engine: cel
  when: >-
    is_last_cycle
    && self_referential_safe
    && qa_file_path.startsWith("docs/qa/")
    && qa_file_path.endsWith(".md")
  reason: "QA testing deferred to final cycle; skips unsafe docs"

Confidence-Based Gating

Skip fix if QA confidence is high enough:

yaml
prehook:
  engine: cel
  when: "qa_confidence != null && qa_confidence < 0.8"
  reason: "QA confidence above threshold — no fix needed"

Build Failure Gate

Only run deployment if build succeeded:

yaml
prehook:
  engine: cel
  when: "build_exit_code != null && build_exit_code == 0"
  reason: "Build must pass before deployment"

CEL Expression Quick Reference

CEL (Common Expression Language) supports standard operations:

cel
# Comparison
cycle > 1
active_ticket_count == 0

# Logical operators
is_last_cycle && qa_failed
fix_required || active_ticket_count > 0

# Null checks (important for optional values)
qa_exit_code != null && qa_exit_code == 0

# String operations
qa_file_path.startsWith("docs/qa/")
qa_file_path.endsWith(".md")
step == "qa_testing"

# Negation
!qa_failed
!(is_last_cycle && fix_required)

Important: Optional integer variables (qa_exit_code, fix_exit_code, etc.) can be null. Always null-check before comparing:

cel
# Wrong — will error if qa_exit_code is null
qa_exit_code == 0

# Correct
qa_exit_code != null && qa_exit_code == 0

Finalize Rules (CEL Context)

Finalize rules use the same CEL engine but with an extended variable set. In addition to the prehook variables above, finalize rules have access to:

VariableTypeDescription
retest_new_ticket_countintTickets created during retest
qa_configuredboolQA step exists in workflow
qa_observedboolQA step was observed in this cycle
qa_enabledboolQA step is enabled
qa_ranboolQA step actually executed
qa_skippedboolQA step was skipped (prehook returned false)
fix_configuredboolFix step exists in workflow
fix_enabledboolFix step is enabled
fix_ranboolFix step executed
fix_skippedboolFix step was skipped
fix_successboolFix completed successfully
retest_enabledboolRetest step is enabled
retest_ranboolRetest executed
retest_successboolRetest passed
is_last_cycleboolWhether this is the final cycle
qa_confidencefloat?Agent QA confidence (also available in prehook context)
qa_quality_scorefloat?Agent quality score (also available in prehook context)
fix_confidencefloat?Fix agent confidence (also available in prehook context)

Default Finalize Rules

If you don't specify custom finalize rules, the engine applies 12 built-in rules in this order (first match wins):

#Rule IDCondition (simplified)Status
1skip_without_ticketsqa_skipped && active_ticket_count == 0 && is_last_cycleskipped
2qa_passed_without_ticketsqa_ran && qa_exit_code == 0 && active_ticket_count == 0qa_passed
3fix_disabled_with_tickets!fix_enabled && active_ticket_count > 0unresolved
4fix_failedfix_ran && !fix_successunresolved
5fixed_without_retestfix_success && !retest_enabledfixed
6fix_skipped_and_retest_disabledfix_enabled && !fix_ran && !retest_enabled && active_ticket_count > 0unresolved
7fixed_retest_skipped_after_fix_successretest_enabled && !retest_ran && fix_successfixed
8unresolved_retest_skipped_without_fixretest_enabled && !retest_ran && !fix_success && active_ticket_count > 0unresolved
9verified_after_retestretest_ran && retest_success && retest_new_ticket_count == 0verified
10unresolved_after_retestretest_ran && (!retest_success || retest_new_ticket_count > 0)unresolved
11fallback_unresolved_with_ticketsactive_ticket_count > 0unresolved
12fallback_qa_passedactive_ticket_count == 0qa_passed

The last two rules are catch-all fallbacks. Custom rules in your workflow's finalize.rules replace these defaults entirely.

Custom Finalize Rules Example

yaml
finalize:
  rules:
    # QA passed cleanly
    - id: qa_clean_pass
      engine: cel
      when: "qa_ran && active_ticket_count == 0"
      status: qa_passed
      reason: "QA passed with no active tickets"

    # Fix verified by retest
    - id: fix_verified
      engine: cel
      when: "fix_ran && retest_ran && retest_success"
      status: fix_verified
      reason: "Fix applied and verified"

    # QA skipped in non-final cycle — keep pending
    - id: qa_deferred
      engine: cel
      when: "qa_skipped && !is_last_cycle"
      status: pending
      reason: "QA deferred to next cycle"

    # Fallback
    - id: fallback
      engine: cel
      when: "true"
      status: pending
      reason: "No rule matched — keep pending"

Next Steps