paultmathew opened a new issue, #66428:
URL: https://github.com/apache/airflow/issues/66428
### 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`):
```tsx
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:
```tsx
- 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
```python
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:
```bash
$ 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?
- [x] Yes I am willing to submit a PR!
### Code of Conduct
- [x] I agree to follow this project's [Code of
Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]