Human-in-the-loop Tasks

Purpose and Scope

Human-in-the-loop, often shortened to HITL, is the Airflow pattern for pausing an automated workflow until a person supplies a decision. In the tutorial spine, this appears alongside TaskFlow and data pipeline tutorials as a way to model approvals, reviews, or other business checkpoints without leaving the orchestration graph. A HITL task is still represented as an Airflow task instance, but the important runtime state is that the task is waiting for input rather than simply computing, retrying, or succeeding. The UI evidence in this repository shows Airflow treating these waiting task instances as required actions that can be listed, filtered, and reviewed by an operator.

Sources: airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.test.tsx, airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.test.tsx

The practical reader problem is not only how to put a review step in a DAG, but how that pause becomes visible and actionable after the DAG run reaches it. The tests for the HITL review modal create a HITLDetail object with a subject, options, created_at, and an embedded task_instance whose state is awaiting_input. Those fields define the UI-facing contract: the reviewer needs a subject that explains the request, a bounded set of responses such as Approve or Reject, and enough task identity to know which DAG, run, and task are affected.

Relevant Source Files

  • airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.test.tsx - Exercises the review dialog, pending versus completed HITL data, response filters, and the minimal HITLDetail shape rendered by the modal.
  • airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.test.tsx - Exercises the HITL task-instance listing page, its generated OpenAPI query hook, auto-refresh behavior, URL search-parameter handling, and a regression around mapped HITL task rows.

Core Primitives

A HITL review is centered on a detail record rather than on a free-form UI-only message. The modal test constructs the record with options: ["Approve", "Reject"], subject: "Test subject", and created_at: "2024-01-01T00:00:00Z". The nested task_instance includes dag_id, dag_run_id, task_id, try_number, map_index, rendered_map_index, run_after, id, and state. This makes review work traceable back to the exact Airflow runtime object that is blocked. For mapped tasks, the map_index and rendered map index matter because a mapped HITL task can have multiple reviewable task-instance rows, not just one logical task.

The page-level primitive is the HITL task-instance listing. In the listing tests, the UI imports useTaskInstanceServiceGetHitlDetails from the generated OpenAPI query module and expects a response shaped like data: { hitl_details: [], total_entries: 0 }. That naming is important for developers extending the page: the list is not assembled from arbitrary task-instance calls, but through a HITL-specific task-instance service query. The test suite stubs DataTable and HITLFilters, which indicates that the page responsibility under test is translating route and search state into the correct listing request rather than validating table rendering.

Sources: airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.test.tsx

UI Review Flow

The review modal opens as a dialog named by the translated key requiredAction_other, which reflects the user-facing concept: these are actions required from a person. When only pending HITL data is supplied, the modal intentionally does not render a completed-response filter. That keeps the default review surface focused on work that still needs attention. When both pending and completed HITL collections are supplied, the modal renders a button named filters.response.all; clicking it changes the view so completed rows are included. The tests assert that pending rows are visible by default and completed rows are hidden until the reviewer switches the filter.

This distinction between pending and completed rows is more than a convenience. In a production Airflow deployment, a reviewer usually needs a short queue of tasks that can be acted on now, while operators and auditors may later need to inspect prior decisions. The checked-in tests encode that behavior at the component boundary by passing pendingHitl and optional completedHitl props into HITLReviewModal. Developers should preserve that separation when changing the modal: pending work should remain the primary path, and completed work should be opt-in unless a broader listing page intentionally requests it.

Sources: airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.test.tsx

System-to-Code Mapping

ConceptSource-backed implementation signalWhy it matters
Review detailHITLDetail with subject, options, created_at, and task_instanceDefines the shape the UI needs to render a human decision request.
Awaiting statetask_instance.state: "awaiting_input"Distinguishes a human-blocked task from normal running, failed, or successful task states.
Pending queuependingHitl={{ data: [mockHitl] }}Keeps actionable reviews visible by default.
Completed queuecompletedHitl={{ data: [completedHitl] }}Enables history or all-response views without mixing them into the default queue.
Listing APIuseTaskInstanceServiceGetHitlDetailsConnects the page to generated OpenAPI queries instead of local-only state.
Refresh cadenceuseAutoRefresh: () => 5000 in the page test mockShows the listing is designed to refresh periodically while reviewers wait for new work.

The listing test also documents a subtle runtime concern for dynamic task mapping. A regression comment explains that the page previously hard-coded mapIndex: parseInt(searchParams.get(MAP_INDEX) ?? "-1", 10), which silently dropped mapped HITL rows. The fix is tested through the parameters sent to the listing API. When extending filters or deep links for this page, treat map_index as an optional, meaningful selector rather than as a harmless default. Sending an artificial -1 can change the server-side interpretation and hide rows produced by mapped HITL tasks.

Sources: airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.test.tsx

Implementation Details for Contributors

The tests rely on react-i18next, react-router-dom, generated OpenAPI query hooks, and shared UI utilities such as Wrapper. They mock translation to return keys directly, which is why assertions use names such as requiredAction_other and filters.response.all. They also preserve most of the generated query module while overriding only useTaskInstanceServiceGetHitlDetails, so unrelated config hooks continue to work through global test handlers. This is a useful pattern for contributors: isolate the HITL behavior under test without replacing infrastructure that the component normally depends on.

For component changes, the modal tests provide the clearest behavioral checklist. Keep the dialog accessible by role, keep completed filters conditional on completed data, show only pending rows by default, and reveal completed rows only after the all-response filter is selected. For page changes, keep URL search parameters, route parameters, OpenAPI query arguments, and auto-refresh behavior aligned. If adding new filter dimensions, add tests around the exact request object sent through useTaskInstanceServiceGetHitlDetails, because the page’s correctness depends on not accidentally excluding valid HITL task instances.

Testing Signals and Next Steps

The repository evidence for HITL is especially valuable because it tests user-observable behavior rather than implementation internals. A failing modal test means reviewers may not see the right queue of required actions. A failing listing-parameter test means whole categories of HITL rows, especially mapped rows, may disappear from the page. Before changing the tutorial, UI labels, filters, or generated API bindings, run the relevant UI test target in the Airflow frontend test environment and inspect both pending-only and all-response flows.

Next, read the TaskFlow tutorial when you need to understand how Python functions become Airflow tasks, then return to this page when adding approval or review pauses to that flow. For runtime behavior, connect HITL tasks to the task-instance model, task logs, and generated REST/OpenAPI surfaces. For UI work, start with the two tests listed above, make the smallest behavioral change, and add a regression assertion whenever a URL parameter, mapped-task identity field, or pending/completed filter changes.