Skip to content

feat(expressions): expose workflow run context as run.* in CEL (swamp-club#331)#1370

Merged
stack72 merged 1 commit into
mainfrom
worktree-331
May 12, 2026
Merged

feat(expressions): expose workflow run context as run.* in CEL (swamp-club#331)#1370
stack72 merged 1 commit into
mainfrom
worktree-331

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented May 12, 2026

Summary

  • Adds a structured run namespace to the CEL expression context so workflow steps can reference run.id, run.workflowName, run.workflowId, run.startedAt, and run.tags in ${{ }} template expressions
  • Fixes the root cause where workflowRunId was set on the context after workflow expressions were eagerly evaluated — run.* and workflowRunId expressions are now deferred to step execution time, matching the existing self.* pattern
  • Enables run-scoped resource keys (e.g. filtered-vms-${{ run.id }}) to prevent collisions during concurrent workflow executions

Test plan

  • Unit tests: run.id, run.workflowName, run.startedAt, run.tags resolution via resolveAllExpressionsInData
  • Unit tests: run.* and workflowRunId expressions skipped during workflow evaluation
  • Integration test: workflow with ${{ run.id }} in step inputs resolves to actual run UUID
  • E2E verification: compiled binary, scratch repo with ${{ run.id }} and ${{ run.workflowName }} — both resolve correctly
  • Full test suite: 5852 passed, 0 failed
  • deno check, deno lint, deno fmt all pass

Closes swamp-club#331

🤖 Generated with Claude Code

…essions (swamp-club#331)

Workflow steps can now reference `run.id`, `run.workflowName`,
`run.workflowId`, `run.startedAt`, and `run.tags` in CEL template
expressions. This enables run-scoped resource keys that prevent
collisions during concurrent workflow executions.

The root cause of the missing variable was timing: WorkflowExpressionEvaluator
eagerly evaluated all expressions before the WorkflowRun was created. The fix
defers `run.*` and `workflowRunId` expressions to step execution time, matching
the existing `self.*` deferral pattern.

Co-authored-by: Blake Irvin <blakeirvin@me.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Well-structured feature that exposes workflow run metadata as run.* in CEL expressions. The implementation correctly defers run.* and workflowRunId expressions to step execution time (matching the existing self.* pattern), and the RunContext interface is a clean value object placed appropriately in the expression domain.

Blocking Issues

None.

Suggestions

  1. Minor: regex could use test() instead of match() — In expression_evaluators.ts:100-101, expr.celExpression.match(/\brun\./) and expr.celExpression.match(/\bworkflowRunId\b/) allocate match arrays that are immediately discarded. Using .test() (e.g., /\brun\./.test(expr.celExpression)) is marginally more efficient and expresses intent more clearly. Same pattern exists for the self.* check on line 94. Non-blocking — consistent with existing code style.

  2. Design doc table formatting — The design/expressions.md table uses Record<string,string> for run.tags type. Consider using backticks around the generic to prevent any markdown rendering ambiguity with the angle brackets if this doc is ever rendered outside of GitHub. Very minor.

Overall: clean implementation, good test coverage across all three layers (expression evaluation service, workflow expression evaluator skip logic, and execution service integration), correct DDD placement of RunContext as a value object in the expressions domain, and the non-null assertion on run.startedAt! is safe since it immediately follows run.start() which sets the field.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None found.

Medium

  1. src/domain/workflows/expression_evaluators.ts:100 — Regex /\brun\./ could false-match inside CEL string literals, deferring unrelated expressions.
    If a user writes a CEL expression containing run. inside a string literal — e.g. ${{ data.query('hasPrefix(name, "run.")') }} — the regex matches the inner string content and incorrectly skips the expression during workflow evaluation. The expression is still evaluated at step execution time (step inputs are always re-resolved), so there's no data loss — only deferred evaluation. This is the same limitation the existing \bself\. check has (line 94), so this PR is consistent with the established pattern. A CEL-aware check would be more robust, but the blast radius here is minimal.

Low

  1. src/domain/workflows/execution_service.ts:1375 — Non-null assertion on run.startedAt!.
    run.start() is called on line 1368 which sets _startedAt = new Date(), so startedAt is guaranteed non-null. The assertion is safe. A defensive ?? new Date().toISOString() fallback would remove the assertion, but this is style, not a bug.

  2. src/domain/workflows/execution_service.ts:1370-1378 — The run expression context is a snapshot, not a live view.
    expressionContext.run is set once after run.start() and captures tags via shallow copy ({ ...run.tags }). If tags were mutated later in the run (they aren't currently), the expression context would be stale. This is the correct design — snapshots avoid concurrency hazards — but worth noting if tags ever become mutable mid-run.

Verdict

PASS — Clean, well-structured change that follows established patterns. The run.* namespace is correctly deferred past workflow-level evaluation, the RunContext interface is appropriately typed, the startedAt timing (set after run.start()) is correct, and the spread-copy of tags prevents aliasing. Test coverage is thorough: unit tests for CEL resolution of all five fields, skip-behavior tests for both run.* and workflowRunId, and an integration test verifying end-to-end resolution against a real WorkflowExecutionService. No blocking issues.

@stack72 stack72 merged commit 8537dac into main May 12, 2026
11 checks passed
@stack72 stack72 deleted the worktree-331 branch May 12, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant