Untangle `workflow_definition` into stable feature ID, engine routing, and config version
<!--IssueSummary start-->
<details>
<summary>
Everyone can contribute. [Help move this issue forward](https://handbook.gitlab.com/handbook/marketing/developer-relations/contributor-success/community-contributors-workflows/#contributor-links) while earning points, leveling up and collecting rewards.
</summary>
- [Label this issue](https://contributors.gitlab.com/manage-issue?action=label&projectId=278964&issueIid=599841)
- [Close this issue](https://contributors.gitlab.com/manage-issue?action=close&projectId=278964&issueIid=599841)
</details>
<!--IssueSummary end-->
> ⚠️ **This issue was drafted with the help of an AI assistant** based on the discussions in !235204. It captures the current understanding but has not yet been independently sanity-checked. Please review the proposed phases, code pointers, and open questions critically before acting on them - some details may be inaccurate or out of date once !235204 merges and rebases.
## Problem
The string `developer/v1` (stored as `Ai::DuoWorkflows::Workflow#workflow_definition` and `Ai::Catalog::FoundationalFlow#foundational_flow_reference`) is currently overloaded with three distinct meanings:
- **Stable feature identity** used as a billing/analytics key (`label` on internal events like `start_duo_workflow_execution`, the `workflow_definition` additional property on `duo_workflow_workload_completed`, audit event `target_details`, and the `WORKFLOW_EVENTS` mapping in [`ee/lib/api/ai/duo_workflows/workflows.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/ai/duo_workflows/workflows.rb)).
- **Engine routing key** for the AI Gateway Flow Registry. Currently derived by splitting the combined string on `/` in [`Ai::DuoWorkflows::FoundationalFlowStartParamsResolver`](https://gitlab.com/gitlab-org/gitlab/-/blob/ts/flow-versioning-support/ee/lib/ai/duo_workflows/foundational_flow_start_params_resolver.rb) (introduced in !235204).
- **Config version**, where the `/v1` suffix is sometimes interpreted as a SemVer-like indicator even though it's actually the registry schema version. The new explicit `flow_version` attribute (e.g. `2.0.0` for `developer/v1`) introduced in !235204 makes that ambiguity concrete.
!235204 introduced explicit `flow_config_id`, `flow_config_schema_version`, and `flow_version` params for the engine side, but left the billing/feature identity coupled to the same combined string. This issue tracks the follow-up cleanup.
Relevant discussion: [thread starting here](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/235204#note_3333134409) (Luke's "stable ID + SemVer + schema version" framing), [Thomas' response](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/235204#note_3333436628) outlining the four-property model, [Mikolaj's `billing_id` / `feature_id` proposal](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/235204#note_3333890030), and [Thomas' observation](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/235204#note_3337942785) that the flow-triggering surface area has grown organically and would benefit from a holistic look.
## Goals
- Introduce a **stable feature ID** that billing/analytics can rely on, decoupled from engine routing decisions.
- Stop deriving `flow_config_id` / `schema_version` by string-splitting `foundational_flow_reference` (currently in [`FoundationalFlowStartParamsResolver#call`](https://gitlab.com/gitlab-org/gitlab/-/blob/ts/flow-versioning-support/ee/lib/ai/duo_workflows/foundational_flow_start_params_resolver.rb)).
- Stay green-deployable: no DB migrations, no ClickHouse changes, no broken historical analytics in the first step.
## Out of scope
- Renaming the `duo_workflows_workflows.workflow_definition` DB column (high cost, low marginal benefit). The column keeps its current name and values; only its *interpretation* by emitters shifts.
- Renaming the public REST param `workflow_definition` (documented in [`doc/api/duo_agent_platform_flows.md`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/api/duo_agent_platform_flows.md)) or the GraphQL field `workflowDefinition` ([`ee/app/graphql/types/ai/duo_workflows/workflow_type.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/graphql/types/ai/duo_workflows/workflow_type.rb)) - separate API contract change.
- A holistic refactor of the flow-triggering paths (`Ai::DuoWorkflows::CreateAndStartWorkflowService`, `Ai::Catalog::ExecuteWorkflowService`, `Ai::Catalog::Flows::ExecuteService`, `Ai::Messaging::TriggerFlowService`, vulnerability triggers). To be tracked separately.
## Proposed approach (phased)
### Phase 1 - explicit attributes on `FoundationalFlow` (no DB change, no external contract change)
1. Add a `feature_id` attribute to [`Ai::Catalog::FoundationalFlow`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/ai/catalog/foundational_flow.rb) with values that visibly differ from `flow_config_id`. Proposed: `duo_developer`, `duo_fix_pipeline`, `duo_code_review`, `duo_sast_fp_detection`, `duo_secrets_fp_detection`, `duo_resolve_sast_vulnerability`, `duo_convert_to_gl_ci`. The `duo_*` prefix follows the precedent of [`SelfHostedBillingTracker::FEATURE_QUALIFIED_NAME`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/ai/code_suggestions/self_hosted_billing_tracker.rb).
2. Add explicit `flow_config_id` and `flow_config_schema_version` attributes on `FoundationalFlow` (default values per flow, no longer derived by parsing `foundational_flow_reference`).
3. Update [`FoundationalFlowStartParamsResolver`](https://gitlab.com/gitlab-org/gitlab/-/blob/ts/flow-versioning-support/ee/lib/ai/duo_workflows/foundational_flow_start_params_resolver.rb) to read those explicit attributes directly rather than splitting on `/`, and to also return `feature_id` in its result hash.
4. Address [Luke's open question about `container` typing](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/235204#note_3337082040): the resolver currently accepts either a `Project` or `Namespace` implicitly. Either rename the arg, add a type guard, or accept both explicitly with documented behavior.
### Phase 2 - dual-emit on billing/analytics events (event schema additions, value preserved for back-compat)
1. Add `feature_id` to `additional_properties` in the relevant event YAMLs: [`start_duo_workflow_execution`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/events/start_duo_workflow_execution.yml), [`finish_duo_workflow_execution`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/events/finish_duo_workflow_execution.yml), [`retry_duo_workflow_execution`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/events/retry_duo_workflow_execution.yml), [`duo_workflow_workload_completed`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/events/duo_workflow_workload_completed.yml), and the `agent_platform_session_*` events.
2. In [`WorkflowEventTracking#workflow_tracking_properties`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/duo_workflows/concerns/workflow_event_tracking.rb) and [`WorkloadMetrics#track_workload_completion_metrics`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/duo_workflows/concerns/workload_metrics.rb), resolve `feature_id` via `FoundationalFlow.find_by_reference(workflow.workflow_definition)&.feature_id` and pass it through. Keep `label: workflow.workflow_definition` for back-compat.
3. For custom flows (`workflow_definition == "ai_catalog_agent"`) and the legacy default (`workflow_definition == "software_development"`), emit shared `feature_id` values (proposal: `duo_custom_flow` and `duo_software_development`).
4. Update [`API::Ai::DuoWorkflows::Workflows#track_event`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/ai/duo_workflows/workflows.rb) (vulnerability flow billing trigger) to look up `feature_id` rather than keying its `WORKFLOW_EVENTS` hash by `foundational_flow_reference`.
5. Migrate dashboards/queries to prefer `feature_id` over `label`.
### Phase 3 - clean up remaining hardcoded references (low-risk hygiene)
1. [`Ai::Messaging::TriggerFlowService::WORKFLOW_DEFINITION_REF = 'developer/v1'`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/messaging/trigger_flow_service.rb) - resolve via `FoundationalFlow` lookup instead of a hardcoded string.
2. [`Ai::FoundationalChatAgent.reference_from_workflow_definition`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/ai/foundational_chat_agent.rb) - audit whether chat agents should also gain a `feature_id` rather than the same split convention.
3. [`Vulnerabilities::TriggeredWorkflow#workflow_name`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/vulnerabilities/triggered_workflow.rb) - this is already a separate enum (`sast_fp_detection`, `resolve_sast_vulnerability`, `secrets_fp_detection`). Useful precedent, confirm alignment with the new `feature_id` values.
### Phase 4 (optional, future) - persist `feature_id` on the workflow row
Only if querying/filtering by stable feature ID becomes necessary. Requires column add + backfill + ClickHouse siphon update + GraphQL field. Defer until Phase 2 dashboards confirm the property works.
## Open questions
- **AI Gateway symmetry.** Thomas [noted](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/235204#note_3333436628) that the AI Gateway likely emits events keyed on `developer/v1`-style strings too. Coordinate with the [ai-assist](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist) team to align on the same `feature_id` concept across both services. Without this, Phase 2 dashboards would still see split series from the gateway side.
- **Feature flag overrides and billing.** Today the `duo_developer_next_unstable` override in [`FoundationalFlowStartParamsResolver`](https://gitlab.com/gitlab-org/gitlab/-/blob/ts/flow-versioning-support/ee/lib/ai/duo_workflows/foundational_flow_start_params_resolver.rb) swaps the reference from `developer/v1` to `developer_unstable/experimental`, which also implicitly swaps the billing label. Should `feature_id` stay `duo_developer` regardless of the override, or should the unstable variant have its own `feature_id`?
- **Custom flow / external agent billing buckets.** Confirm with product that one shared `feature_id` per flow type (one for custom, one for external agents) is sufficient, vs needing per-item granularity. Per current understanding, no sub-classification needed.
- **`ai_catalog_item_version_id` scope.** It's a GitLab-side audit/billing/UI concept and is **not** sent to the Duo Workflow Service. Confirm it stays that way and is not surfaced on engine env vars.
## Downstream usage inventory (informational - to be verified during implementation)
Places where the combined `workflow_definition` string is read today:
- [`Ai::DuoWorkflows::Workflow#workflow_definition`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/ai/duo_workflows/workflow.rb) - persisted text column (default `'software_development'`, 255-char check constraint), replicated to ClickHouse `siphon_duo_workflows_workflows`.
- [`Ai::DuoWorkflows::Concerns::WorkflowEventTracking#workflow_tracking_properties`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/duo_workflows/concerns/workflow_event_tracking.rb) - Snowplow `label`.
- [`Ai::DuoWorkflows::Concerns::WorkloadMetrics#track_workload_completion_metrics`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/duo_workflows/concerns/workload_metrics.rb) - Snowplow `workflow_definition` additional property.
- [`Ai::DuoWorkflows::UpdateWorkflowStatusService#audit_event_for_status_change`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/duo_workflows/update_workflow_status_service.rb) - audit event `target_details`.
- [`API::Ai::DuoWorkflows::Workflows#track_event` and `WORKFLOW_EVENTS`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/ai/duo_workflows/workflows.rb) - vulnerability flow billing.
- [`Vulnerabilities::TriggeredWorkflow#workflow_name`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/vulnerabilities/triggered_workflow.rb) - already a separate enum.
- [`Ai::Catalog::ExecuteWorkflowService#determine_workflow_definition`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/catalog/execute_workflow_service.rb) - sets `'ai_catalog_agent'` for custom flows.
- [`Ai::Catalog::Flows::ExecuteService#fetch_flow_definition`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/catalog/flows/execute_service.rb) - foundational vs custom branching.
- [`Ai::Messaging::TriggerFlowService::WORKFLOW_DEFINITION_REF`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/ai/messaging/trigger_flow_service.rb) - hardcoded `'developer/v1'`.
- [`Ai::FoundationalChatAgent.reference_from_workflow_definition`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/models/ai/foundational_chat_agent.rb) - chat agent split logic.
- GraphQL: [`DuoWorkflow.workflowDefinition` field](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/graphql/types/ai/duo_workflows/workflow_type.rb), [`create` mutation arg](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/graphql/mutations/ai/duo_workflows/create.rb), [`workflows` resolver `type` filter](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/graphql/resolvers/ai/duo_workflows/workflows_resolver.rb).
- REST: `workflow_definition` POST param at `/api/v4/ai/duo_workflows/workflows` ([docs](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/api/duo_agent_platform_flows.md)).
- Background migration `backfill_service_account_id_on_duo_workflows_workflows` (uses workflow_definition + ai_catalog_item_version_id).
- [`scripts/dap_foundational_flows_release_notes.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/dap_foundational_flows_release_notes.rb) (parses `foundational_flow.rb` for references).
## Related
- !235204 (Support flow versioning for foundational flows)
- &18785 (Flow versioning epic)
issue