Apache Airflow version
3.2.0 (also reproduced on main and v3-2-stable by source inspection)
What happened?
Any HITL task instance with map_index >= 0 (i.e. produced by a mapped task or a task inside a mapped @task_group via expand_kwargs / expand) is silently invisible on every Required Actions listing view in the UI:
Browse → Required Actions (/required_actions)
- Per-DAG
Required Actions tab (/dags/<dag_id>/required_actions)
- Per-Run
Required Actions tab (/dags/<dag_id>/runs/<run_id>/required_actions)
- Per-Task
Required Actions tab (/dags/<dag_id>/tasks/<task_id>/required_actions)
The home-page "pending required actions" badge counts the row correctly (its SQL — responded_at IS NULL AND state = DEFERRED in airflow/api_fastapi/common/parameters.py:774-783 — does not filter on map_index), so the user sees a count of N pending actions but the listing pages are empty. The per-task-instance Required Actions tab (/dags/<dag_id>/runs/<run_id>/tasks/<task_id>/mapped/<map_index>/required_actions) works because it uses the singular /taskInstances/{task_id}/{map_index}/hitlDetails endpoint instead of the listing endpoint.
Root cause
airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx (identical in main, v3-2-stable, and v3-1-stable):
const mapIndex = searchParams.get(MAP_INDEX) ?? "-1";
…
const { data, error, isLoading } = useTaskInstanceServiceGetHitlDetails(
{
…
mapIndex: parseInt(mapIndex, 10),
…
},
…
);
When the URL has no map_index query param (the default for every entrypoint into the page), mapIndex is the literal string "-1", then parsed and sent as a hard map_index=-1 filter on the listing API call. -1 is the sentinel for non-mapped task instances, so any mapped HITL row is silently dropped server-side.
The getQueryString helper in airflow-core/src/airflow/ui/openapi-gen/requests/core/request.ts:48 only skips undefined and null values, so parseInt("-1", 10) = -1 is always serialized into the request URL.
This regression was introduced by #55776 (feat(hitl): add map_index filter to get_hitl_details endpoint, merged 2025-09-18, shipped in 3.1.0) and is still present on main today.
What you think should happen instead?
When the URL does not contain map_index, the listing call should not send a map_index filter at all, so HITL rows for both mapped and unmapped tasks show up. The map_index query parameter should remain available as an opt-in filter for users who want to drill down to a specific map index.
Suggested patch:
- const mapIndex = searchParams.get(MAP_INDEX) ?? "-1";
+ const mapIndexParam = searchParams.get(MAP_INDEX);
…
- mapIndex: parseInt(mapIndex, 10),
+ mapIndex: mapIndexParam !== null ? parseInt(mapIndexParam, 10) : undefined,
How to reproduce
import pendulum
from airflow.providers.standard.operators.hitl import ApprovalOperator
from airflow.sdk import DAG, task_group
with DAG(
dag_id="hitl_mapped_repro",
schedule=None,
start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
):
@task_group(group_id="g")
def per_item(name: str):
ApprovalOperator(task_id="review", subject=f"review {name}", fail_on_reject=True)
per_item.expand(name=["a", "b", "c"])
Trigger the DAG. The 3 g.review instances all defer (map_index = 0, 1, 2). The home-page "Pending required actions" badge shows 3. Browse → Required Actions shows "No Required Actions found". Direct API calls confirm the data is present and the UI default is what hides it:
$ curl "$AF/api/v2/dags/~/dagRuns/~/hitlDetails?response_received=false&state=deferred" | jq '.total_entries'
3
$ curl "$AF/api/v2/dags/~/dagRuns/~/hitlDetails?response_received=false&state=deferred&map_index=-1" | jq '.total_entries'
0 # ← what the UI sends by default
$ curl "$AF/api/v2/dags/~/dagRuns/~/hitlDetails?response_received=false&state=deferred&map_index=0" | jq '.total_entries'
1 # ← row IS in the DB and reachable, just hidden by the UI's default filter
There is no URL value the user can set that disables the filter — the API parameter is strictly typed int | None, so empty string / null / NaN / wildcard values all 422; and parseInt(string, 10) in the UI always yields a number, so the OpenAPI client never produces undefined (the only value that would skip serialization in request.ts:48).
Operating system
Reproduced on Linux container (apache/airflow:3.2.0-python3.12) running locally via Docker Compose. Source inspection confirms the bug is identical on main and v3-2-stable.
Versions of Apache Airflow Providers
Standard provider with HITLOperator / ApprovalOperator (any version that supports HITL).
Deployment
Other Docker-based deployment (Astronomer-style local setup with LocalExecutor).
Anything else?
- The same component also has a small auto-refresh predicate bug:
detail.responded_at === undefined should compare to null (the API serializes the field as JSON null, not omitted), so even when the listing query is fixed the page won't auto-refresh while waiting for new actions. Happy to roll that into the same PR if maintainers are OK with it.
- The "pending required actions" badge subquery in
airflow/api_fastapi/common/parameters.py:774-783 does not include a map_index predicate, which is why it disagrees with the listing — that asymmetry is what makes the bug user-visible (UI shows "1 pending" badge but empty listing).
Are you willing to submit PR?
Code of Conduct
Apache Airflow version
3.2.0 (also reproduced on
mainandv3-2-stableby source inspection)What happened?
Any HITL task instance with
map_index >= 0(i.e. produced by a mapped task or a task inside a mapped@task_groupviaexpand_kwargs/expand) is silently invisible on every Required Actions listing view in the UI:Browse → Required Actions(/required_actions)Required Actionstab (/dags/<dag_id>/required_actions)Required Actionstab (/dags/<dag_id>/runs/<run_id>/required_actions)Required Actionstab (/dags/<dag_id>/tasks/<task_id>/required_actions)The home-page "pending required actions" badge counts the row correctly (its SQL —
responded_at IS NULL AND state = DEFERREDinairflow/api_fastapi/common/parameters.py:774-783— does not filter onmap_index), so the user sees a count of N pending actions but the listing pages are empty. The per-task-instance Required Actions tab (/dags/<dag_id>/runs/<run_id>/tasks/<task_id>/mapped/<map_index>/required_actions) works because it uses the singular/taskInstances/{task_id}/{map_index}/hitlDetailsendpoint instead of the listing endpoint.Root cause
airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx(identical inmain,v3-2-stable, andv3-1-stable):When the URL has no
map_indexquery param (the default for every entrypoint into the page),mapIndexis the literal string"-1", then parsed and sent as a hardmap_index=-1filter on the listing API call.-1is the sentinel for non-mapped task instances, so any mapped HITL row is silently dropped server-side.The
getQueryStringhelper inairflow-core/src/airflow/ui/openapi-gen/requests/core/request.ts:48only skipsundefinedandnullvalues, soparseInt("-1", 10) = -1is always serialized into the request URL.This regression was introduced by #55776 (
feat(hitl): add map_index filter to get_hitl_details endpoint, merged 2025-09-18, shipped in 3.1.0) and is still present onmaintoday.What you think should happen instead?
When the URL does not contain
map_index, the listing call should not send amap_indexfilter at all, so HITL rows for both mapped and unmapped tasks show up. Themap_indexquery parameter should remain available as an opt-in filter for users who want to drill down to a specific map index.Suggested patch:
How to reproduce
Trigger the DAG. The 3
g.reviewinstances all defer (map_index = 0, 1, 2). The home-page "Pending required actions" badge shows3.Browse → Required Actionsshows "No Required Actions found". Direct API calls confirm the data is present and the UI default is what hides it:There is no URL value the user can set that disables the filter — the API parameter is strictly typed
int | None, so empty string /null/NaN/ wildcard values all 422; andparseInt(string, 10)in the UI always yields a number, so the OpenAPI client never producesundefined(the only value that would skip serialization inrequest.ts:48).Operating system
Reproduced on Linux container (
apache/airflow:3.2.0-python3.12) running locally via Docker Compose. Source inspection confirms the bug is identical onmainandv3-2-stable.Versions of Apache Airflow Providers
Standard provider with
HITLOperator/ApprovalOperator(any version that supports HITL).Deployment
Other Docker-based deployment (Astronomer-style local setup with
LocalExecutor).Anything else?
detail.responded_at === undefinedshould compare tonull(the API serializes the field as JSONnull, not omitted), so even when the listing query is fixed the page won't auto-refresh while waiting for new actions. Happy to roll that into the same PR if maintainers are OK with it.airflow/api_fastapi/common/parameters.py:774-783does not include amap_indexpredicate, which is why it disagrees with the listing — that asymmetry is what makes the bug user-visible (UI shows "1 pending" badge but empty listing).Are you willing to submit PR?
Code of Conduct