Skip to content

feat(workflows): expose {{ context.run_id }} template variable#2664

Open
doquanghuy wants to merge 1 commit into
github:mainfrom
doquanghuy:feat/expose-run-id-template-var
Open

feat(workflows): expose {{ context.run_id }} template variable#2664
doquanghuy wants to merge 1 commit into
github:mainfrom
doquanghuy:feat/expose-run-id-template-var

Conversation

@doquanghuy
Copy link
Copy Markdown

Description

Closes #2590.

Surfaces the engine-assigned run id (the same 8-character hex
string Spec Kit prints as Run ID: at the end of
workflow run) as a workflow template variable so YAML authors
can reference it from shell run:, command input.args:,
switch expression:, and any other field that already evaluates
{{ ... }} templates.

This is shape A from the issue ({{ context.run_id }}) —
the most discoverable option and consistent with the existing
inputs.* / steps.X.output.* naming.

Why

The run id is the natural join key between a Spec Kit workflow
run and downstream artifacts, telemetry, or per-run scratch
state. Today the operator sees it in stdout but workflows
themselves cannot reference it — there was no way to stamp a
log line, name a scratch directory, or tag an artifact with the
same id Spec Kit assigned.

The three use cases from the issue:

  1. Telemetry / observability — stamp logs and events with
    the run id so external systems can join workflow runs to
    downstream artifacts.
  2. Per-run scratch / isolation — interactive operator
    commands that need their own state directory under
    /tmp/run-<id>/.
  3. Run-id in artifact metadata — stable join key from
    artifact back to the producing run.

Canonical usage

# Stamp telemetry events with the run id for cross-system join.
- id: emit-event
  type: shell
  run: 'echo "{\"run_id\":\"{{ context.run_id }}\",\"event\":\"started\"}" >> events.jsonl'

# Per-run scratch directory.
- id: prep-scratch
  type: shell
  run: 'mkdir -p /tmp/run-{{ context.run_id }}'

# Pass run id into a command for artifact metadata.
- id: tag-artifact
  command: speckit.specify
  input:
    args: "{{ context.run_id }}"

Implementation

StepContext.run_id is already populated by WorkflowEngine
in both execute() and resume(). The only gap was the
template namespace builder.

_build_namespace (in workflows/expressions.py) now adds a
context key alongside the existing inputs, steps, item,
and fan_in namespaces:

ns["context"] = {"run_id": run_id}

The value is always present (even outside a run) and falls back
to an empty string when no run is active. Workflows referencing
{{ context.run_id }} therefore never error — a hard
requirement from the issue's acceptance criteria for dry-run,
validation, and ad-hoc evaluator usage.

Default behaviour preserved

Workflows that do not reference {{ context.run_id }} are
byte-equivalent to before this change. The context namespace
is added unconditionally to keep template resolution
branch-free, but its presence has no observable effect when
nothing references it.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync && uv run pytest
    2967 passed, 35 skipped (was 2960 before; +7 new
    tests added in this PR).
  • Tested with a sample workflow: ran a shell step with
    run: 'echo "RUN_ID={{ context.run_id }}"' and confirmed
    the captured stdout matches the Run ID: line Spec Kit
    prints at the end of workflow run. Re-ran without the
    template reference and the workflow behaved identically
    to pre-PR.

New test coverage

TestExpressions (unit-level):

Test What it locks
test_context_run_id_resolves Direct lookup against StepContext(run_id=...).
test_context_run_id_defaults_to_empty_when_unset Graceful default outside a run context (no error).
test_context_run_id_string_interpolation Mixed template like "RUN_ID={{ context.run_id }}".

TestContextRunId (end-to-end), covering the three step types the issue's acceptance criteria called out:

Test What it locks
test_shell_run_resolves_run_id run: field substitution, verified via captured stdout.
test_command_input_args_resolves_run_id input.args: resolution, captured in step output even when CLI dispatch is unavailable (the artifact-metadata use case).
test_switch_expression_matches_on_run_id Switch matches against the resolved value, proving the run id is a first-class value in the expression engine.
test_workflow_without_context_reference_unchanged Locks the byte-equivalent default for workflows that don't use the variable.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (described below)

Used Claude Opus to draft the namespace change, the test suite,
the docs section, and this PR body. The shape
({{ context.run_id }} with empty-string fallback) was
proposed in the issue body; this PR implements that proposal.
Code, tests, and design decisions were human-reviewed before
submission.

@doquanghuy doquanghuy requested a review from mnriem as a code owner May 21, 2026 15:04
Closes github#2590.

Surfaces the engine-assigned run id (the same 8-character hex
string Spec Kit prints as `Run ID:` at the end of
`workflow run`) as a workflow template variable so YAML
authors can reference it from shell `run:`, command
`input.args:`, switch `expression:`, and any other field that
already evaluates `{{ ... }}` templates.

### Why

The run id is the natural join key between a Spec Kit workflow
run and downstream artifacts, telemetry, or per-run scratch
state. Today the operator sees it in stdout but workflows
themselves cannot reference it — there was no way to stamp a
log line, name a scratch directory, or tag an artifact with
the same id Spec Kit assigned.

The three motivating use cases from the issue:

1. Telemetry / observability — stamp logs and events with the
   run id so external systems can join workflow runs to
   downstream artifacts.
2. Per-run scratch / isolation — interactive operator commands
   that need their own state directory under
   `/tmp/run-<id>/`.
3. Run-id in artifact metadata — stable join key from artifact
   back to the producing run.

### Implementation

`StepContext.run_id` is already populated by `WorkflowEngine`
in both `execute()` and `resume()`. The only gap was the
template namespace builder.

`_build_namespace` (in `workflows/expressions.py`) now adds a
`context` key alongside the existing `inputs`, `steps`,
`item`, and `fan_in` namespaces:

```python
ns["context"] = {"run_id": run_id}
```

The value is always present (even outside a run) and falls
back to an empty string when no run is active. Workflows
referencing `{{ context.run_id }}` therefore never error — a
hard requirement from the issue's acceptance criteria for
dry-run, validation, and ad-hoc evaluator usage.

### Default behaviour preserved

Workflows that do not reference `{{ context.run_id }}` are
byte-equivalent to before this change. The `context`
namespace is added unconditionally to keep template
resolution branch-free, but its presence has no observable
effect when nothing references it.

### Tests

`TestExpressions` (unit-level) gains three tests:

- `test_context_run_id_resolves` — direct lookup against a
  `StepContext(run_id=...)`.
- `test_context_run_id_defaults_to_empty_when_unset` —
  graceful default outside a run context.
- `test_context_run_id_string_interpolation` — mixed
  template (e.g. `"RUN_ID={{ context.run_id }}"`).

`TestContextRunId` (end-to-end) covers the three step types
the acceptance criteria called out:

- `test_shell_run_resolves_run_id` — `run:` field
  substitution, verified via captured stdout.
- `test_command_input_args_resolves_run_id` — `input.args:`
  resolution, captured in step output even when CLI dispatch
  is unavailable (the artifact-metadata use case).
- `test_switch_expression_matches_on_run_id` — switch
  matches against the resolved value, proving the run id is a
  first-class value in the expression engine, not just an
  interpolation token.
- `test_workflow_without_context_reference_unchanged` —
  locks the byte-equivalent default required by the issue.

### Docs

`workflows/README.md` gains a "Runtime Context" subsection
under "Expressions" documenting the new namespace and the
three canonical use patterns (telemetry, per-run scratch,
artifact metadata).
@doquanghuy doquanghuy force-pushed the feat/expose-run-id-template-var branch from 9940396 to 68634d8 Compare May 21, 2026 15:11
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.

[Feature]: Expose run_id as a workflow template variable

1 participant