DAP reviewer assignment: resolve foundational flow additional context in the trigger path
Phase A. Blocks #603494 and the removal of the project setting.
Problem
The Recommend Reviewers flow needs reviewer_data (current reviewers, approval rules, ranked candidates) to do anything useful. That payload is produced by the flow item's additional_context_resolver:
ee/app/models/ai/catalog/foundational_flow/items.rb:200-204—recommend_reviewers/v1definesadditional_context_resolvercallingAi::DuoWorkflows::RecommendReviewers::ReviewerDataBuilder
FoundationalFlow#resolve_additional_context_for (ee/app/models/ai/catalog/foundational_flow.rb:86-95) is called from exactly one place:
ee/app/services/ai/duo_workflows/create_and_start_workflow_service.rb:128— the bespoke path
The DAP trigger path never calls it. Ai::FlowTriggers::RunService#additional_context (ee/app/services/ai/flow_triggers/run_service.rb:422-437) returns early unless the resource is a Ci::Pipeline, then hardcodes pipeline and merge request URL context.
So a trigger-driven run of Recommend Reviewers receives no reviewer_data, and the flow has nothing to reason over.
This is not a latent gap. Neither the trigger worker nor the catalog item is gated by dap_powered_recommend_reviewers, so any group with beta flows enabled can configure the Merge request > Marked ready trigger today and get exactly these empty runs.
Feasibility — verified, the transport already exists end to end
Both paths converge on the same delivery mechanism, so no new plumbing is needed:
RunService#start_catalog_workflowalready passesadditional_context:(run_service.rb:232) →Ai::Catalog::Flows::ExecuteService(ee/app/services/ai/catalog/flows/execute_service.rb:18, 89) →Ai::Catalog::ExecuteWorkflowService(ee/app/services/ai/catalog/execute_workflow_service.rb:29, 145) →Ai::DuoWorkflows::StartWorkflowService, which serializes it into theDUO_WORKFLOW_ADDITIONAL_CONTEXT_CONTENTworkload variable (ee/app/services/ai/duo_workflows/start_workflow_service.rb:297, 550-579).- The bespoke path uses the same
StartWorkflowServiceand the same variable (create_and_start_workflow_service.rb:42-45, 128) — this mechanism carriesreviewer_datain production today. - The envelope formats match:
resolve_additional_context_forwraps the resolver'sHash[category => content]into the[{"Category" => ..., "Content" => json}]array thatserialized_flow_additional_contextexpects and thatRunServicealready builds for pipelines. - The flow formally consumes it:
duo_workflow_service/agent_platform/v1/flows/configs/recommend_reviewers/1.0.0.ymlingitlab-org/gitlab-ai-gatewaydeclaresflow.inputs: category: reviewer_data(with an input schema) and bindscontext:inputs.reviewer_datain thepost_recommendationandassign_reviewerscomponents.
Prior art: draft !242137 (closed) implemented this exact change in June (as an additional_context_builder hook, with passing specs) before additional_context_resolver existed on master (added by cfe7397b52ae, 2026-07-14, wired only into the bespoke path). This issue is effectively rebasing that MR's RunService half onto the resolver that now exists.
Implementation
In ee/app/services/ai/flow_triggers/run_service.rb, generalize #additional_context (currently lines 422-437):
def additional_context
contexts = []
contexts.concat(pipeline_additional_context) if resource.is_a?(::Ci::Pipeline)
contexts.concat(foundational_flow.resolve_additional_context_for(resource: resource)) if foundational_flow
contexts.presence
end- Extract the existing pipeline envelopes into
pipeline_additional_contextunchanged. resolve_additional_context_forreturns[]for flows with no resolver (context = {} → .map → []), so every other foundational flow is a no-op — no behaviour change outside flows that declare a resolver.- The resolver is called with
resource:only (no user), exactly as the bespoke path calls it —ReviewerDataBuilder.build(resource)needs no user context. foundational_flowis already memoized in this service (run_service.rb:404-409).
Specs:
ee/spec/services/ai/flow_triggers/run_service_spec.rb— trigger-driven run of a foundational flow with a resolver receives the envelopes;Ci::Pipelineresources get pipeline context unchanged (and merged with resolver output if the flow declares one); flows without a resolver getnilas today.- Reuse the spec shapes from !242137 (closed).
Coordination / caveats
- Pending conflict: unmerged commit
769b3540cf38(Paulo Martins, security-review custom instructions) adds another one-off special case to this same method (security_review_flow?branch). The resolver approach subsumes it —security_review/v1could declare anadditional_context_resolvercallingCodeReview::CustomInstructionsContextBuilderinstead. Coordinate before both land to avoid stacking special cases. - Latent bug worth fixing in passing:
StartWorkflowService#serialized_flow_additional_contextdrops caller envelopes whose category collides with reserved categories usingenvelope[:Category](symbol key,start_workflow_service.rb:556-558), but every producer builds string-keyed"Category"envelopes — the guard is currently a no-op. Normalize the key access while in there (or useenvelope["Category"] || envelope[:Category]). - The resolver now runs in the Sidekiq trigger worker for every matching trigger fire;
ReviewerDataBuilderdoes non-trivial work (approval rules, candidate ranking). This is the same cost the bespoke worker pays today, but it lands on theai_catalog_flowsqueue instead — no action needed, just expected load shift.
Acceptance criteria
- A trigger-driven run of
recommend_reviewers/v1receives the samereviewer_dataadditional context as the bespoke path - Existing pipeline additional context is unchanged for
Ci::Pipelineresources - Flows with no
additional_context_resolverare unaffected - Reserved-category guard in
serialized_flow_additional_contextworks for string-keyed envelopes - Verified end to end on a real merge request, not only in specs