Skip to content

feat(workflow): split X-Workflow-ID into definition + execution headers (closes #185)#197

Merged
initializ-mk merged 1 commit into
mainfrom
feat/issue-185-workflow-execution-id
Jun 25, 2026
Merged

feat(workflow): split X-Workflow-ID into definition + execution headers (closes #185)#197
initializ-mk merged 1 commit into
mainfrom
feat/issue-185-workflow-execution-id

Conversation

@initializ-mk

Copy link
Copy Markdown
Contributor

Summary

Splits the previously-overloaded X-Workflow-ID header into two distinct identifiers so audit consumers can answer per-run AND per-definition queries from one event stream without joining on opaque ids:

  • X-Workflow-ID — workflow definition (stable across all runs of the same workflow)
  • X-Workflow-Execution-IDper-run instance (unique per workflow execution) — new

Mirrored on every audit event as top-level workflow_id (definition semantics) + new workflow_execution_id (per-run semantics).

Why

The code comment said "orchestrator workflow run" but the field name read like a definition identifier — a SIEM looking at the stream couldn't tell whether workflow_id was the join column for "this specific run" or "this workflow over time." This PR disambiguates the two axes following industry precedent:

  • GitHub Actions: workflow + workflow_run_id
  • Tekton: Pipeline + PipelineRun
  • Argo: Workflow + WorkflowRun

Both forms surface so operators get both queries cheaply:

  • "Show me every event for this run" → join on workflow_execution_id
  • "Top failing workflows" / "Latency by workflow definition" → group by workflow_id

Changes

Header convention

Header Audit field Semantics
X-Workflow-ID workflow_id Workflow definition — stable across runs
X-Workflow-Execution-ID (new) workflow_execution_id (new) Per-run instance — unique per execution
X-Workflow-Stage-ID stage_id unchanged
X-Workflow-Step-ID step_id unchanged
X-Invocation-Caller invocation_caller unchanged

Code

  • forge-core/runtime/workflow.goHeaderWorkflowExecutionID const; WorkflowContext.WorkflowExecutionID field; WorkflowContextFromHTTPHeaders / ApplyToHTTPHeaders read+write the new header; IsZero covers it.
  • forge-core/runtime/audit.goAuditEvent.WorkflowExecutionID (json:"workflow_execution_id,omitempty"); EmitFromContext stamps both fields from ctx.
  • forge-core/observability/attrs.go — new AttrForgeWorkflowExecutionID = "forge.workflow.execution.id" span attribute.
  • forge-cli/server/a2a_server.go — dispatcher span carries both attrs when the inbound carries the headers.
  • forge-cli/runtime/runner.go — auth-audit emit sites pass WorkflowExecutionID from wc through to auth_verify / auth_fail events.

Docs

  • docs/security/workflow-correlation.md — headers table + "Why definition + execution" callout with industry precedent; updated audit-event example; updated wiring map.
  • docs/security/audit-logging.md — schema-contract table, workflow-correlation prose, and FWS-3 attribution prose.
  • .claude/skills/forge.md — FWS-2 reference + audit-event field list + initiative table.
  • CHANGELOG — Unreleased entry.

Safety

  • Clean break per the issue — no backward-compat alias since the contract is pre-production.
  • Both new field uses omitempty — direct A2A invocations without orchestrator headers continue to emit byte-identical JSON to pre-FWS-2 consumers. schema_version stays 1.0 (additive schema-compatible change).
  • Definition and execution ids are independent: setting one doesn't auto-derive the other. Pinned by TestWorkflowContextFromHTTPHeaders_DefinitionAndExecutionAreIndependent.

Test plan

  • golangci-lint run across all four modules — 0 issues
  • gofmt -w across all modules
  • go test ./... in forge-core/ and forge-cli/ — all green
  • New unit tests pin: definition + execution independence, full-five-field round-trip through headers → ctx → headers, ApplyToHTTPHeaders populates the new header, EmitFromContext stamps both fields, empty-ctx still omits both keys.
  • Manual smoke: dispatch a tasks/send with both headers; confirm every emitted audit event carries both workflow_id and workflow_execution_id at the top level.

Cross-repo coordination

  • initializ/agent-orchestrator (ORCH-1): dispatcher must populate both headers.
  • initializ/security (SEC-1): typed fields for the new audit column.
  • initializ/console (CON-1): workflow timeline UI consumes workflow_execution_id.

This Forge PR is the foundational naming change the other repos plug into.

…rs (closes #185)

The previously-overloaded X-Workflow-ID header conflated two distinct
queries operators actually want: "show me this specific run" (per-run
timeline) and "show me every run of this workflow" / "top failing
workflows" (per-definition rollup). The code comment said
"orchestrator workflow run" but the name read like a definition
identifier, and there was no way to surface both axes on a single
audit event.

FORGE-2 splits the header into two distinct identifiers, mirroring
industry precedent (GitHub Actions workflow + workflow_run_id, Tekton
Pipeline + PipelineRun, Argo Workflow + WorkflowRun):

- X-Workflow-ID is now the workflow DEFINITION id, stable across all
  runs of the same workflow.
- X-Workflow-Execution-ID is the new per-run instance id, unique per
  workflow execution.

Both surface as top-level fields on every audit event under a workflow
run (workflow_id, workflow_execution_id) so SIEM consumers can build
"per-run timeline" views (join on workflow_execution_id) AND
"definition-level rollup" views (group by workflow_id) without joining
on opaque ids.

Clean break per the issue — no backward-compatibility alias since the
contract is pre-production. Schema stays at version 1.0 because both
new fields are omitempty and direct A2A invocations without
orchestrator headers continue to emit byte-identical JSON.

Pinned by TestWorkflowContextFromHTTPHeaders_{DefinitionAndExecutionAreIndependent,
ExtractsAllFour}, TestApplyToHTTPHeaders_PopulatesExecutionID,
TestRoundTripHTTPHeaders,
TestEmitFromContext_{TagsWorkflowFieldsWhenContextHasThem,
TagsBothWorkflowDefinitionAndExecution,
OmitsWorkflowFieldsWhenContextEmpty}.

Also stamps a new forge.workflow.execution.id OTel span attribute on
the A2A dispatcher span (alongside the existing forge.workflow.id) so
trace browsers can pivot on either axis.
@initializ-mk initializ-mk merged commit 1b03556 into main Jun 25, 2026
10 checks passed
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