Home DevOps Airflow HITL: The Schema Change That Skipped Review
Advanced 6 min · August 1, 2026
Airflow HITL Approval

Airflow HITL: The Schema Change That Skipped Review

Airflow HITL approval gates stop destructive DDL from running unattended.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
Before you start⏱ 15-20 min read
  • Airflow 3.0+ (HITLOperator is an Airflow 3 feature).
  • Familiarity with task dependencies and trigger rules.
  • A Slack or email webhook for notifications.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Airflow HITL (human-in-the-loop) is the Airflow 3 feature that inserts a human approval step before a task runs.
  • Key pieces: the HITLOperator task, its approve/deny UI, notifications to Slack or email, and a timeout that fails the gate.
  • A schema migration that once ran unattended at 2 AM now waits on a named approver with an audit trail.
  • Timeout is the safety net: approval tasks expire, and the pipeline fails loudly instead of stalling silently.
  • Gate destructive steps only — DDL, prod deploys, money movement — or approval fatigue makes the gate theater.
  • Set defaults and the timeout applies it automatically: an expiring gate can auto-reject (or auto-approve) instead of failing.
✦ Definition~90s read
What is Airflow HITL Approval?

Airflow HITL (human-in-the-loop) is the Airflow 3 mechanism that pauses a pipeline on a HITLOperator approval task until a named human approves or denies it, with notifications, timeouts, and an audit trail.

A night shift worker was changing the shape of the warehouse's only door while everyone was asleep.
Plain-English First

A night shift worker was changing the shape of the warehouse's only door while everyone was asleep. HITL is the security guard who must sign a clipboard before any door gets modified: the worker can prep the new door, but it doesn't get installed until the guard checks the plan and writes their name. If no guard shows up, the job stops and pages someone — it never quietly changes the door.

Every team that runs destructive DDL in production eventually has the same nightmare. A migration task that was 'scheduled for the night window' executes unattended, a breaking schema change ships, and the warehouse degrades for the next week — while everyone was asleep.

Airflow's answer is human-in-the-loop: a task that pauses the DAG and asks a real person to approve or deny before the dangerous step runs. In Airflow 3 it's a first-class citizen — the HITLOperator — with its own UI, notifications, and timeout semantics.

The pattern is older than Airflow. Deploy tools, change boards, and money systems have gated approvals for decades; orchestration platforms are finally catching up.

The trap is building gates that can be skipped, expired, or ignored.

HITL isn't bureaucracy. It's the difference between a review that happens and a review that's supposed to happen.

1. Why Human Gates Exist in Pipelines

Some operations are cheap to reverse and some aren't. Dropping a column, deploying to production, moving money — the blast radius is wide, the rollback is painful, and the context needed to make the call is precisely what a pipeline doesn't have. Automation guarantees the operation runs correctly. It can't guarantee the operation should run at all.

That's the argument for human gates: a point in the DAG where progress requires a judgment call that a schedule can't make. Code review works the same way — a second set of eyes before the irreversible step, with a record of who signed off.

The failure mode to fear isn't slowness. It's theater — gates that exist on paper but are skippable in practice, approved by rote, or bypassed 'just this once' because the branch was simpler.

Mental Model
Deployment gates, not checkboxes
A gate is only real if the pipeline physically cannot continue without the human.
  • Irreversible step + unattended run = incident waiting for a night window.
  • A gate is a hard dependency in the DAG, never a notification that might arrive.
  • Audit trail is what turns a gate from a formality into evidence.
📊 Production Insight
Automation runs steps; humans decide whether to run them.
Gate irreversible work: DDL, prod deploys, money.
A skippable gate is worse than no gate — it's fake safety.
🎯 Key Takeaway
Human gates exist for irreversible operations.
The gate must be a hard dependency in the graph.
Audit trail makes the gate real.
Why Human Gates Exist in Pipelines Why Human Gates Exist in Pipelines Automation runs a step; humans decide if it runs IRREVERSIBLE OPERATIONS THE HUMAN GATE Drop a column Wide blast radius Deploy to production Painful rollback Move money Judgment call needed The gate: a hard dependency DAG cannot continue without a human decision Audit: who, when, why A skippable gate is fake safety DDL ran unreviewed at 3 AM THECODEFORGE.IO
thecodeforge.io
Airflow Hitl Approval

2. HITLOperator: The Approve/Deny Task

Airflow 3 ships HITLOperator, an operator that defers the pipeline until a human responds. It appears in the UI with its own approve and deny actions, so an operator on call can act without leaving the browser. On approve, the DAG proceeds to the next task; on deny, the gate fails and downstream tasks don't run.

The operator defers like a deferrable operator — no worker slot is burned while waiting — and the approval event resumes the task. That means a gate can wait for hours without consuming executor capacity, which is exactly what an overnight approval window needs.

Treat the gate as the controlling task: wire everything dangerous downstream of it, and wire nothing that could bypass it. The approval is a binary decision with a comment; make sure the comment is captured, because it's the 'why' your post-mortem will ask for.

HITL is a family, not one operator (Airflow 3.1+, standard provider): HITLOperator is the base class for arbitrary options, ApprovalOperator restricts to Approve/Reject, HITLBranchOperator routes to a downstream task of the human's choice, and HITLEntryOperator collects free-form input through a params form — the standard tool for LLM workflows that need human guidance. The decision travels as XCom: chosen options land under the chosen_options key and form input under params_input, which downstream tasks pull to branch on or record. One semantic to know: ApprovalOperator treats Reject as success-with-skip — the gate task succeeds and everything downstream is skipped; pass fail_on_reject=True when a rejection must fail the run.

Pending gates surface on the UI's Required Actions page, and the same requests are reachable through the REST API — GET /api/v2/hitlDetails/ lists them, PATCH /api/v2/hitlDetails/{dag_id}/{dag_run_id}/{task_id} submits the answer — so approvals work without a browser, from a script or a Slack app. In Airflow 3.3+, a waiting HITL task uses a dedicated scheduler-managed awaiting_input state that holds no worker slot and no triggerer (the triggerer can scale to zero while gates wait); on 3.1/3.2 it defers onto the triggerer.

approval_gate.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from datetime import datetime, timedelta

from airflow import DAG
from airflow.operators.hitl import HITLOperator
from airflow.providers.postgres.operators.postgres import PostgresOperator

with DAG(
    dag_id="migration_orders_v2",
    schedule="0 3 * * *",
    start_date=datetime(2026, 1, 1),
    catchup=False,
) as dag:
    approval = HITLOperator(
        task_id="approve_schema_change",
        notification_webhook_url=SLACK_WEBHOOK_URL,
        message="Approve dropping column legacy_promo from prod.orders?",
        timeout=60 * 60 * 6,  # 6 hours, aligned with on-call SLA
    )

    run_migration = PostgresOperator(
        task_id="run_migration",
        postgres_conn_id="postgres_prod",
        sql="ALTER TABLE prod.orders DROP COLUMN legacy_promo;",
    )

    approval >> run_migration
Output
02:31 UI: approval task deferred · Slack: 'Approve dropping column...' · 08:10 approved by sarah.d → run_migration runs
📊 Production Insight
HITLOperator defers — no worker slot while waiting.
Approve proceeds; deny fails the gate and stops downstream.
Keep the comment; it's the 'why' for the post-mortem.
🎯 Key Takeaway
HITLOperator pauses the DAG for a human decision.
UI approve/deny plus webhook notification.
Everything dangerous sits downstream of the gate.
HITLOperator: The Approve/Deny Task HITLOperator: The Approve/Deny Task Deferred wait — no worker slot burned Night migration DAG Scheduled 3 AM night window HITLOperator approval gate Deferred · no worker slot held Approve in the UI DAG proceeds downstream Deny in the UI Gate fails · no downstream Approved by sarah.d → migration ran Every decision logged: who, when, why THECODEFORGE.IO
thecodeforge.io
Airflow Hitl Approval

3. Wiring Notifications: UI, Slack, Email

A gate nobody sees is a gate nobody approves. The HITLOperator supports a notification webhook, and the integration of choice in 2026 is Slack — the approval message lands in the channel where the on-call engineer actually lives. Email is a fallback, not a strategy; inboxes are where approvals go to die.

Route to a named approver or a rotation, never a shared inbox. 'Data platform' channels are where everyone assumes someone else will click. The gate message must name the person on call, include the context (what changes, what breaks if wrong), and link back to the DAG run.

Escalation is the second half of notification design. If no one responds within a fraction of the timeout, the gate should page the next tier. A single message that goes unanswered for six hours is a stalled pipeline; a message that escalates at 30 minutes becomes a decision.

The operator API wires this through notifiers: a list of callables invoked when a request starts waiting, succeeds, or fails (the docs' LocalLogNotifier shows the interface; a notifier that POSTs to a Slack webhook is the production pattern). Restrict who may respond with assigned_users — a list of {id, name} pairs — so only named approvers can act on the gate, and the approver field in the audit trail is always a real person.

escalation_flow.pyPYTHON
1
2
3
4
5
6
7
8
9
10
# Slack side (incoming webhook app):
# 1. approval task defers -> webhook posts to #data-oncall
#    "Approve schema change on prod.orders? (sarah.d on call)"
# 2. 30 min later, no response -> workflow posts to #data-escalation
#    "Still waiting on approval — 30 min left before timeout"
# 3. approve/deny button click -> webhook back to Airflow
#    -> task resumes with event {approved: true, approver: 'sarah.d', comment: '...'}
#
# Airflow side: HITLOperator(notification_webhook_url=..., timeout=3600*6)
# Approval decisions log to metadata DB with timestamps.
Output
#data-oncall 02:31 · #data-escalation 03:01 · approved 03:12
📊 Production Insight
Notify where the on-call engineer lives — Slack.
Name the approver; shared inboxes never approve.
Escalate at 30 minutes, not at timeout.
🎯 Key Takeaway
Unseen gates never get approved.
Route to named approvers with context and links.
Escalation is half the notification design.
Notify + Escalate: A Gate Someone Sees Notify + Escalate: A Gate Someone Sees Slack webhook → on-call channel → next tier Gate defers · webhook fires HITLOperator notification webhook #data-oncall: 'Approve schema change?' Names sarah.d on call · links DAG run 30 min no response → #data-escalation Pages the next tier, not the timeout Approve/deny → webhook back to Airflow Task resumes with approver + comment Shared inboxes are where approvals die Escalate at 30 min, not at timeout THECODEFORGE.IO
thecodeforge.io
Airflow Hitl Approval

4. Timeout and Expiry of Approval Tasks

An approval that waits forever is a pipeline outage in slow motion. The HITLOperator timeout bounds the wait: on expiry the gate fails, downstream tasks don't run, and the failure is visible — loudly, with an alert path if you wired notifications correctly. That's the design intent: stale approvals become loud failures, not silent stalls.

The timeout should match the operation's SLA, not your optimism. An overnight migration gate can wait six hours; a prod deploy gate should force a decision in minutes. When the timeout and the SLA disagree, the gate is lying to the business about when decisions happen.

Two failure paths need planning. Denied: the DAG fails with a clear message and an audit record — decide whether a retry is allowed or the run is abandoned. Timed out: the pipeline failed without a human decision, which is the outcome you designed for; the runbook should say who gets paged and what the recovery rerun looks like.

Timeout doesn't have to mean failure: set defaults to one of the options and the timer applies it automatically — an approval gate can auto-reject (defaults=["Reject"]) or auto-approve at expiry, converting a missed decision into a defined outcome instead of a failure. The timeout argument is response_timeout on Airflow 3.3+ (3.1/3.2 used execution_timeout), and since 3.3 the scheduler's response-timeout sweep enforces it even while no triggerer is held.

⚠ Timeout is the anti-stall guarantee
An approval task without a timeout is a permanent block waiting for a response that may never come. Every gate ships with a timeout, an escalation, and a defined failure path.
📊 Production Insight
Timeout converts stale approvals into loud failures.
Size the timeout to the operation's SLA.
Plan the deny path and the timeout path explicitly.
🎯 Key Takeaway
Every gate needs a timeout bound.
Expiry fails the gate; denial fails the gate.
Define recovery for both before shipping.

5. Where HITL Fits: Destructive DDL, Prod Deploys, Money Movement

HITL earns its keep on the operations where a wrong answer is expensive and a right answer requires judgment. Destructive DDL — drops, truncates, renames — tops the list: the schema change that skipped review in this article's incident is the canonical case. Production deploys run a close second, and money movement third: payments, refunds, batch settlements where an unattended error is a financial incident.

The line is reversibility. If the operation can be undone cheaply and safely, automation wins — a gate there is friction without protection. If the operation is one-way, gate it. If the operation is one-way and expensive to undo, gate it with dual approval and a longer review window.

The pattern composes. A deploy pipeline can carry several gates: one before the migration, one before the cutover, one before the rollback decision. Each gate is cheap to run — it defers, not blocks — so the cost is human attention, which is exactly the resource that should be spent.

📊 Production Insight
Gate irreversible work: DDL, deploys, money.
Skip gates where rollback is cheap.
Dual approval for expensive-to-undo operations.
🎯 Key Takeaway
Reversibility draws the HITL line.
Gate one-way operations; automate the rest.
Multiple gates compose without blocking slots.

6. HITL vs Sensors vs External Waits

Three tools pause a pipeline, and teams routinely use the wrong one. A sensor waits for a condition in the world — a file, an API, a partition — and resumes automatically when it appears. HITL waits for a human decision, which can't be detected by polling. ExternalTaskSensor waits for another DAG's task to finish, a pure scheduling dependency.

The test is what completes the wait. If a machine can check it, it's a sensor. If a person must decide, it's HITL. If it's about ordering between DAGs, it's a cross-DAG dependency. Mixing them up creates two failure classes: pipelines that stall waiting for a human when a file check would do, and pipelines that skip the human entirely because someone modeled the approval as a sensor condition.

📊 Production Insight
Machine-checkable waits are sensors.
Judgment calls are HITL; ordering is ExternalTaskSensor.
Wrong tool = stall or skipped review.
🎯 Key Takeaway
Sensor: condition. HITL: decision. External: ordering.
Ask what completes the wait to pick the tool.
Never model approval as a sensor condition.

7. Designing the Approval Contract

A gate is only as good as the contract around it. Write it down before the DAG: who approves (named role, not 'the team'), what they approve (exact scope of the change), how long they have (timeout tied to SLA), what happens on deny, and where the decision is recorded. The contract is what makes the gate auditable and the approver accountable.

Operationally, three things keep the contract honest. First, the gate must be structurally unskippable — no trigger rule that bypasses it, no conditional branch around it, no 'approve if the ticket exists' shortcut. Second, decisions log identity and comment, because post-incident reviews are won or lost on that record. Third, test the gate's failure modes in staging: deny, timeout, and no-notification, so the failure path is proven before the first real deployment.

This is the difference between review theater and review. The schema change that skipped review happened because the contract was implicit. Make it explicit, and the gate stops being a formality and starts being a control.

📊 Production Insight
Write the approval contract before the DAG.
Make the gate structurally unskippable.
Test deny, timeout, and silence paths in staging.
🎯 Key Takeaway
Approval contracts name approver, scope, and SLA.
Unskippable gates + logged decisions = real control.
Test failure modes before the first real gate.
● Production incidentPOST-MORTEMseverity: high

The Schema Change That Skipped Review

Symptom
The analytics team reported missing columns and broken dashboards the morning after a routine 'night window' migration DAG ran.
Assumption
The team assumed the migration task had been reviewed and approved during the day, because 'someone must have signed off' in the change ticket.
Root cause
The migration task ran as an ordinary task in the night window with no approval gate. The change ticket was never actually reviewed — nothing in the pipeline required a human decision, so the DDL executed unattended and unreviewed.
Fix
Inserted a HITL approval task before the destructive DDL, wired it to Slack with the on-call engineer as approver, and set a timeout that fails the DAG if approval doesn't land. The migration now ships only when a named human approves — and every decision is auditable.
Key lesson
  • Automation doesn't create review — a pipeline that can run unattended will run unattended.
  • An approval gate must be a hard dependency, not a side-channel ticket or a chat message.
  • Timeouts turn stale approvals into loud failures instead of silent stalls.
  • Every approve/deny decision needs an audit trail: who, when, and why.
Production debug guideSymptom to Action5 entries
Symptom · 01
Approval task stuck for hours
Fix
Check the timeout and whether the notifier actually delivered. Most stalls are missed Slack notifications, not Airflow issues — check the on-call rotation.
Symptom · 02
Task failed with a deferred timeout after someone approved
Fix
The approver responded after the timeout expired. Rerun the DAG or clear the approval task; align the timeout with the SLA in the runbook.
Symptom · 03
Nobody knows a task is waiting for approval
Fix
Notifications weren't wired to a real channel. Verify the webhook integration and route to the on-call rotation, not a shared inbox.
Symptom · 04
Approver says they approved, pipeline still failed
Fix
The task after the gate may be failing for another reason. Inspect the gate task log for the approval event, then check the downstream task.
Symptom · 05
Audit trail is empty
Fix
Approval metadata wasn't logged. Make the operator record approver identity, timestamp, and comment to the metadata DB or an audit sink.
HITL vs Sensor vs External Wait
AspectHITL approvalSensor waitExternalTaskSensor
What it waits forA named human's approve/deny decisionAn external condition (file, API, partition)Another DAG's task completion
Blocking stateDeferred — no worker slot heldDeferred or poke (configurable)Scheduled, waiting on the upstream run
Human interaction requiredYes — someone must click approve or denyNo — resumes automatically when trueNo — pure scheduling dependency
Typical timeoutMinutes to hours, aligned with on-call SLASeconds to hoursUpstream DAG run window
Audit requirementsHigh — who, when, and why mattersLowLow
Failure modeTimeout or explicit deny fails the gateTimeout fails the sensorMissing or mismatched external_dag_id

Key takeaways

1
HITL approval gates make destructive steps wait on a named human with an audit trail.
2
A gate without a timeout is a permanent block; a gate without notifications is a silent one.
3
Approval is a hard dependency
never branch around it with trigger rules.
4
Gate only what's irreversible
DDL, prod deploys, money movement.
5
Log every approve/deny
who, when, and why — that's what incident reviews ask for.

Common mistakes to avoid

4 patterns
×

No timeout on the approval task

Symptom
A waiting approval blocks the pipeline for days, silently
Fix
Set a timeout tied to the operation SLA and route the failure to on-call.
×

Notifications to a shared inbox

Symptom
Nobody approves; everyone assumes someone else will
Fix
Notify the named approver and the on-call rotation, with escalation.
×

HITL on every task

Symptom
Approval fatigue — people click approve without reading
Fix
Gate only destructive or irreversible steps; automate the rest.
×

Expecting Reject to fail the DAG run

Symptom
The run ends success with skipped tasks after a rejection, and nothing visibly failed
Fix
Know ApprovalOperator semantics: Reject succeeds the gate and skips everything downstream; pass fail_on_reject=True when a rejection must fail the run.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is human-in-the-loop in Airflow and why do pipelines need it?
Q02SENIOR
Walk through how you'd add an approval gate to a nightly DDL migration.
Q03SENIOR
A breaking schema change slipped past your HITL gate. How do you make th...
Q01 of 03JUNIOR

What is human-in-the-loop in Airflow and why do pipelines need it?

ANSWER
HITL is Airflow 3's mechanism for pausing a pipeline until a human decides: the HITLOperator defers the run, notifies an approver, and resumes on approve or fails on deny or timeout. Pipelines need it for irreversible operations — destructive DDL, production deploys, money movement — where automation can guarantee execution but not judgment, and where an audit trail is required.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is HITL in Airflow 3?
02
How do I get notified when a task waits for approval?
03
What happens if nobody approves before the timeout?
04
Can HITL replace sensors?
05
Where should HITL gates be used — and not used?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
August 01, 2026
last updated
1,750
articles · all by Naren
🔥

That's Airflow. Mark it forged?

6 min read · try the examples if you haven't

Previous
Airflow Object Storage
35 / 37 · Airflow
Next
Airflow vs Prefect vs Dagster