Airflow HITL Approval: Stop Unreviewed DDL From Shipping
Airflow HITL approval gates stop unreviewed DDL with named approvers and audit trails.
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
- ✓An Airflow 3.x deployment with the standard provider installed
- ✓A DAG with a step risky enough to deserve review (DDL, deploy)
- ✓Slack or email notifier access for approval pings
- Airflow HITL approval gates pause the DAG for a named human decision before destructive steps like DDL or deploys
- Key components: ApprovalOperator and HITLOperator, assigned_users, notifiers with UI links, response_timeout with Reject defaults
- Performance insight: waiting approvals park in awaiting_input holding zero worker slots, so 100 pending gates cost nothing while they wait
- Production insight: one unattended night-window migration broke downstream pipelines for hours; a gate would have held it for one review
- Biggest mistake: approval gates with no notifiers, leaving decisions waiting in a UI the approvers never open
Think of a demolition crew that needs a signed permit before knocking down a wall, not after. The crew (your pipeline) prepares everything, then waits. A named inspector (your approver) reviews the plan, signs approve or deny, and the decision is filed permanently. No signature, no demolition.
Some pipeline steps shouldn't run themselves. Dropping a column, migrating a billion-row table, or moving money deserves a human looking at the plan before the machine executes it.
One team's DDL migration ran unattended in the night window. The schema change broke every downstream pipeline, and review happened after the damage, in the postmortem. Automation did exactly what it was told.
HITL gates insert a person at the dangerous step. Approve or deny, named approver, full audit trail. Machines propose. Humans dispose.
Why Human Gates Exist in Pipelines
Pipelines execute; humans judge. Data arrival is a condition a sensor can watch, but dropping a column is a decision carrying accountability no trigger can own. You'll draw the line at irreversibility: reversible steps automate, irreversible steps ask.
Regulated teams feel it first. Finance, health, and payments pipelines need a named person owning each dangerous action for auditors. But every team benefits the second a night-window migration breaks three downstream owners.
Count your irreversible steps. DDL, prod deploys, money movement, bulk deletes: each gets a gate. Everything else keeps flowing untouched, so the pipeline stays fast where it's safe.
HITLOperator: The Approve and Deny Task
ApprovalOperator is the approve-or-deny task. It renders a subject plus body, offers Approve and Reject, restricts responses to assigned_users, and records the decider with a timestamp. Place it directly before the dangerous task so nothing slips between review and execution.
Defaults carry the safety. defaults='Reject' means silence, timeout, or confusion all resolve to no. You'll pair it with a response_timeout measured in hours, not days, so forgotten gates fail safe instead of parking forever.
Assigned users carry the accountability. Named owners from the team that understands the migration, not a group alias. The audit trail names a person, which is exactly what the postmortem and the auditor both ask for.
Know what each operator is. HITLOperator is the base: subject plus options (single, multiple=True for multi-select, defaults for the pre-checked choice, params_input for free-form human input that flows to downstream XCom). ApprovalOperator narrows it to Approve/Reject — and Reject terminates rather than continuing, so reach for HITLBranchOperator when a rejection should run cleanup tasks instead of stopping. HITLEntryOperator covers the data-entry shape. Lock sensitive gates with assigned_users=[{id, name}, ...] — only listed users can respond, and both fields are required.
The big 3.3 change: waiting HITL tasks now sit in a dedicated scheduler-managed awaiting_input state instead of deferring onto the triggerer. While parked they hold neither a worker slot nor triggerer capacity, so the triggerer can scale to zero with approvals still pending; tasks resume on human response or the scheduler's response-timeout sweep. On 3.1/3.2 the same operators use the older trigger-based deferral, which is why version-pinned docs disagree about triggerer sizing for HITL.
Wiring Notifications That Reach Humans
Approvers live in Slack and email, not in the Airflow UI. Notifiers bridge the gap by posting the gate's subject plus a direct UI response link the moment the task parks. You'll attach them on gate creation, success, and failure so no state change goes quiet.
Write the notification like a page. What needs review, where to click, how long before it auto-rejects. An approver triaging Slack at midnight decides in seconds with that format and ignores vague pings.
Test the path with a dry gate. Trigger a test approval on staging monthly and confirm the Slack message, the UI link, and the email all arrive. Rotated webhooks silently break this path, and silent gates recreate the incident with better intentions.
Wire notifiers for the request moment, not just success/failure: the notifiers=[...] list fires when the HITL request goes pending, and HITLOperator.generate_link_to_ui_from_context builds the direct response link for Slack/email. Humans answer on the Required Actions page or via REST — PATCH /api/v2/dags/{dag_id}/dagRuns/{run_id}/taskInstances/{task_id}/{map_index}/hitlDetails with {chosen_options, params_input} — and GET .../hitlDetails?response_received=false lists what's still waiting. Locally, airflow dags test parks at awaiting_input and waits for that same UI/API response (response_timeout isn't enforced without a scheduler, so the test waits indefinitely until you answer) — which also lets an agent drive the loop: watch for the waiting log line, ask the human, submit via API.
Don't confuse this with agentic HITL Review on AgentOperator (common-ai provider): that loop polls XCom with time.sleep and holds its worker slot for the whole review (30+ minutes of occupied slot at a 10s poll). Standard-provider HITL parks slot-free. Pick standard HITL for gates; pick Review only when an LLM output needs conversational approve/reject/regenerate rounds.
Timeout and Expiry of Approval Tasks
Timeouts bound the wait; expiry defines the default. Four hours fits a business-day review, overnight windows need morning deadlines, and money movement may deserve minutes. You'll set each gate's window from the decision's real urgency.
Defaults decide what silence means. Reject is the safe answer for destructive steps: the migration waits for explicit approval or it doesn't run. Narrow informational inputs may default differently, but danger always defaults to no.
Escalate on expiry, don't just stop. A timed-out gate should page the secondary owner with the same review packet, not vanish into a skipped task. The first timeout teaches you the real review latency; adjust windows with that data.
Name the timeout right for your version: response_timeout is the 3.3+ waiter (defaults → scheduler sweep), while 3.1/3.2-era examples show execution_timeout on the same operators. Either way, pair every gate with defaults (what happens when nobody answers) so expiry is a decision, not a hang. DDL-until-approved, deploy-until-signed, money-movement-until-two-eyes — each gets subject, body with the XCom-rendered diff, assigned_users, response_timeout, and defaults, all five or the gate isn't prod.
Where HITL Fits: DDL, Deploys, Money
HITL fits where judgment outweighs checkability. Destructive DDL needs a human who understands blast radius. Prod deploys need an owner accepting the release. Money movement needs a signer auditors can name. Each gate buys accountability automation cannot.
You'll skip gates where conditions suffice. Data arrival, partition completeness, and row-count bands are sensor and gate-task territory. Humans decide what only humans can own; everything checkable stays automated.
Review the gate list quarterly. Steps that grew safe automation graduate out; new irreversible steps graduate in. A gate inventory that never changes is either perfect or unexamined.
HITL Versus Sensors and External Waits
Sensors watch the world; HITL watches humans. A sensor resumes when a file lands whether or not anyone cares. A gate resumes when a named person accepts responsibility. You'll pick by asking who owns a wrong outcome.
External waits without gates are the legacy pattern: a human watches a dashboard, then clicks something elsewhere. HITL folds the click into the DAG with identity, timeout, and audit. Migration means replacing chat approvals with gate tasks one pipeline at a time.
Never stack both blindly. A gate followed by a sensor (approve, then wait for the file) models reality; a sensor followed by a redundant gate just slows mornings. Each wait should answer a different question.
The Schema Change That Skipped Review
- Approving code is not approving execution. Destructive steps need a live gate at run time, not just a merged pull request from last week.
- Gates need named deciders and safe defaults. Anyone-can-approve is theater, and waiting forever is a parked pipeline, not caution.
- Notify the decider where they work. An approval visible only in the Airflow UI waits on a notification gap, not a judgment gap.
| File | Command / Code | Purpose |
|---|---|---|
| dags | from airflow.sdk import dag, task | HITLOperator |
| dags | from airflow.providers.standard.operators.hitl import HITLOperator | Wiring Notifications That Reach Humans |
Key takeaways
Common mistakes to avoid
4 patternsRunning destructive DDL with no human gate
Approval gates with no timeout or default
Letting anyone in the org approve anything
Gates that notify nobody
Interview Questions on This Topic
Design a human gate before a destructive migration. What makes it safe versus theater?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.
That's Airflow. Mark it forged?
4 min read · try the examples if you haven't