Push dap_for_each feature flag to the AI gateway
What
Adds the dap_for_each feature-flag definition and the push_feature_flag call that carries it to the
AI gateway, so for_each can be exercised by real flows. The flag ships enabled by default — it is
there to switch the mechanism off if it misbehaves, not to stage a rollout.
Why
ai-assist!6814
added the for_each fan-out component attribute on the gateway side, gated behind dap_for_each. A
gateway-side flag needs both halves to work: the gate in the gateway, and the definition plus
push_feature_flag here — the gateway only sees the flags the monolith sends it. !6814 (merged) landed the
gateway half and left the monolith half as the deliberate next step; its own lib/feature_flags/context.py
says so on the enum member:
Not yet defined in gitlab/config/feature_flags -- to be added alongside the first flow config that opts in.
This MR is that next step. The same comment sits on DAP_PARALLEL_SUBAGENTS four lines below, and that
flag was defined in the monolith standalone anyway (!251134 (merged)),
with no shipped flow config declaring parallel_subagents either — so landing the monolith half ahead of
the first adopter is the established pattern in this family, not a departure from it.
Until this merges the gateway resolves dap_for_each as disabled, so a component declaring a for_each:
block runs once, unwrapped, and the flow completes normally — the only trace is a gateway-side warning log.
Flag state — on by default
type: beta, default_enabled: true, rollout issue #630214.
- Why on by default.
for_eachonly changes behaviour for a component that explicitly declares afor_each:block; a component without one never reaches the flag check. No flow config shipped in ai-assist declares one today, so the flag being on is inert until a flow adopts the attribute — there is no incremental percentage worth ramping. - Why the flag exists. A central off-switch if a fan-out misbehaves. A fanned-out component runs its
whole body per item (
max_itemsdefault 1000,max_concurrencydefault 10), so the cost blast radius is real enough to want one. - Why not
experiment, like its siblings.experiment,wipandgitlab_com_deriskcannot bedefault_enabled: true—Feature::Definition#validate_default_enabled!raises and Danger fails the MR. - Why
betaand notops.opswould also permit default-on and would drop the removal clock, but it is for long-lived operational controls; this flag is expected to be temporary and removed oncefor_eachhas been exercised by a real flow.betasays that out loud, at the cost of a rollout issue and a 6-month removal horizon. Happy to switch toopsif you would rather not carry the clock. - On Danger's "release the feature with the feature flag" note. The usual path is to ship a flag off, roll it out, then flip the default. This is a new flag, never set on any environment, so nothing is being flipped out from under an existing rollout and the default is what governs. No docs change is needed either — the "All feature flags in GitLab" page is generated from the YAML at docs-build time.
How
Wired exactly as its siblings (dap_parallel_subagents, dap_schema_auto_tool_choice,
dap_workspace_agents), plus a spec asserting the push — which those siblings have and this one lacked.
The push is user-scoped (current_user), matching all three siblings and sitting above this method's
return unless root_namespace guard, so it is pushed on every DAP request rather than only those carrying
a root namespace.
Scope
Not specific to any one flow — for_each is a platform primitive and any consumer needs this definition.
It is deliberately not part of the BL Security Analyzer work, but BLSA depends on it: BLSA's move to
the experimental component generation adopts for_each for its fan-out stages, so that work cannot land
until this flag exists. !246889 (BLSA
monolith integration) is the dependent MR and currently targets this branch. Product context:
&23435.
Testing
- The new spec in
ee/spec/lib/api/helpers/duo_workflow_helpers_spec.rbfails on its assertion if thepush_feature_flagcall is removed — the stub accepts any arguments, so the example fails onhave_received(...).with(:dap_for_each, user)rather than erroring. It also pins placement: it callspush_feature_flagswith no arguments, so moving the line below theroot_namespaceguard or into theduo_developerbranch would fail it too. - Pipeline 2869370151 is green on
e194d572, zero failed jobs — includingrspec:feature-flagsandfeature-flags-usage, which confirm the definition is matched to its usage and that itstypeagrees with its directory.
Known limitations
- Flow configs do not only come from the ai-assist repo. "No shipped config declares
for_each:" covers files in that repo. A customer-authored AI Catalog flow is passed through asflow_config(ee/app/services/ai/catalog/execute_workflow_service.rb), so a Catalog flow declaringfor_each:runs once today and fans out once this merges. That is the intended capability, not a defect, but it is the concrete path worth knowing. - For self-managed, the off-switch is per-instance. With
default_enabled: truethe flag is on for self-managed too, and there is no central control there — disabling it is an instance-admin action, not something we can flip remotely. - The inert window has a known end. ai-assist!6322
(BLSA engine, still draft) adds
flows/configs/bl_security/2.0.0-foreach.yml, the first shipped config to declarefor_each:. When that lands, the fan-out becomes live behaviour rather than a dormant capability. - Turning the flag back off is not perfectly transparent. While on, an unknown key inside a
for_each:block fails the flow at load time; while off the block is ignored and nothing inside it is validated. So disabling reverts an adopting component to a single unwrapped run rather than erroring.
Merge order
!246889 currently targets this branch, so
this MR merges first and that one is retargeted to master after. Because of that stacking the branch is
merged with master rather than rebased, so Danger warns about the master-merge commit's subject
length and missing body. Squash-on-merge is on, so the landed commit takes the MR title and is unaffected.
ee/config/feature_flags/experiment/dap_for_each.yml. Because this MR moves the definition to beta/,
the two files sit at different paths and git reports no conflict — so !246889 must delete its copy
before it merges, or master ends up with two definitions of dap_for_each and Feature::Definition
raises. Tracked on that MR, not this one.