How to Build Self-Healing Airflow Pipelines With an AI Agent Safely
Five layers, one policy table, and the points where a human still approves
The safe way to build self-healing Airflow pipelines with an AI agent is to let the model classify failures and let a separate policy layer decide what may run. In the Astronomer Data Flowcast episode on ATC Drivetrain, self-healing is described as five layers (observe, classify, policy, recover, validate), and the episode says about 95% of small failure modes recover on their own. The AI never bypasses operational policy or approval steps. This guide turns that structure into a concrete Airflow design.
Key Takeaways
- •Self-healing means recovering from a known failure without paging an engineer, and only after a policy check says recovery is allowed.
- •The model should only classify failures and gather context; a deterministic policy table, not the model, decides which actions may run.
- •Airflow's on_failure_callback is the natural observe hook, and since Airflow 2.6.0 it accepts a list of functions, so detection and notification can be separate callbacks.
- •Every recovery action needs a validation step; a retry that succeeds on a corrupted input is not a recovery.
- •Schema changes, backfills over a wide date range, and anything touching PII or financial tables should stay human-approved.
Why Airflow Self-Healing Needs Policy, Not Just a Model
Airflow already retries tasks. What it cannot do is tell a transient network timeout from a column that was renamed upstream. Both raise an exception, and both land in the same failed task state. An AI agent is good at that distinction because it can read a stack trace, the surrounding logs, and recent deploys together. The risk is the next step: if the same model that diagnosed the failure also chooses and runs the fix, a wrong diagnosis becomes a wrong action with production credentials.
We separate the two jobs. The model produces a structured classification with a confidence score and a short rationale. A policy table, written by humans and versioned in git, maps each failure class to an allowed action, a bounded number of attempts, and an approval requirement. The agent can propose anything, but only actions that appear in the table can execute. This is the same boundary the Astronomer episode describes when it says AI-driven steps are treated as bounded, governed workflow components with human approval and confidence-based stop conditions.
This also changes how you debug. When something goes wrong, you ask two separate questions: was the classification wrong, or was the policy too permissive? Mixing them into one prompt makes that impossible to answer. Keeping them apart is what lets you tighten one without retraining or re-prompting the other, and it is what an auditor will want to see. We covered the audit side in what a data agent audit trail must record.
The Five Layers: Observe, Classify, Policy, Recover, Validate
The five layers come from the Astronomer episode, which defines self-healing as identifying a known failure, deciding whether recovery is safe, running an approved action, and validating the result. The episode summary does not describe each layer's internals, so the Airflow mapping below is our own implementation of that structure, not a description of how ATC Drivetrain built theirs.
| Layer | Job | Airflow mechanism | Who decides |
|---|---|---|---|
| Observe | Capture failure with run context | on_failure_callback, task logs, metadata DB | Airflow |
| Classify | Label the failure class and confidence | LLM call over exception, logs, recent changes | Model |
| Policy | Decide if recovery is allowed | Versioned policy table checked in code | Humans (written), code (enforced) |
| Recover | Run one bounded, approved action | Clear task, retry with delay, rerun bounded window | Executor with scoped credentials |
| Validate | Confirm the fix actually worked | Row counts, freshness check, downstream task success | Deterministic checks |
Notice that only one of the five layers involves a model. Observation, policy, recovery, and validation are ordinary, testable code. That is deliberate. The more of the loop you can make deterministic, the smaller the surface where a probabilistic component can hurt you, and the easier each layer is to unit test.
The layers also fail independently. If classification is unavailable because the model endpoint is down, the system should fall through to the default Airflow behavior: retries, then an alert. If policy has no rule for a failure class, the answer is always "page a human." Self-healing should degrade to the pipeline you already had, never to something worse.
Observe and Classify With an on_failure_callback
The observe layer is a callback. Per the Airflow callbacks documentation, on_failure_callback runs at the Dag or task level, and since Airflow 2.6.0 you can pass a list of functions. That lets you keep the observer small and separate from your pager notification. The callback should only package context and hand it off. It should never mutate Airflow state itself.
python
def observe_failure(context):
ti = context['task_instance']
event = {
'dag_id': ti.dag_id,
'task_id': ti.task_id,
'run_id': context['run_id'],
'try_number': ti.try_number,
'exception': str(context.get('exception'))[:2000],
}
submit_to_classifier(event) # async, no state changes here
default_args = {
'retries': 2,
'on_failure_callback': [observe_failure, page_if_unclassified],
}
Two caveats from the same documentation matter here. Callbacks run only when a worker executes and changes state, so tasks cleared or failed manually through the CLI or UI do not trigger them. And errors inside a callback show up in the dag processor logs rather than the task logs, so a broken observer fails quietly unless you watch those logs. Add a heartbeat check that confirms events are actually reaching the classifier.
The classifier receives the event plus context: the last few log lines, recent deploys, and upstream task states. It returns a failure class from a closed list, such as transient_network, upstream_late, schema_drift, resource_exhaustion, or unknown. Force the output into that enum. A free-text answer cannot be matched against a policy table, and "unknown" must be a first-class, expected result.
Policy as a Table the Model Cannot Edit
The policy layer is where safety lives. We write it as a small, reviewed file in the same repository as the Dags, so every change goes through pull request review. The agent can read it but has no write path to it. Each rule names a failure class, one allowed action, a hard attempt limit, a minimum classifier confidence, and whether a human must approve.
yaml
rules:
- failure_class: transient_network
action: retry_task
max_attempts: 2
min_confidence: 0.85
approval: none
- failure_class: upstream_late
action: clear_and_rerun_task
max_attempts: 1
min_confidence: 0.90
approval: none
- failure_class: schema_drift
action: propose_only
approval: human
- failure_class: unknown
action: page_oncall
The confidence thresholds above are illustrative starting points, not benchmarks; set yours from your own replayed failures. The important properties are structural. Every rule is a single named action, not a free-form instruction. Attempt limits are enforced by the executor, not requested of the model. Anything not listed falls to the unknown rule. Combine this with a deny list for tables or Dags that must never auto-recover, such as finance close jobs or anything writing PII.
If you want an agent to help author this table, have it draft rules from historical incidents and open a pull request. A human reviews and merges. That keeps the model useful for the tedious work of enumerating failure modes while keeping authority over what is allowed firmly with your team.
Recover and Validate: Small Actions, Checked Results
Recovery actions should be the smallest thing that could work: clear a single failed task, retry after a delay, or rerun one bounded date window. Avoid actions that fan out, such as clearing a Dag across weeks of history. Wide backfills belong in a separate flow with their own limits, which we describe in our guide to safe, bounded backfills. Run recovery with a service account scoped to Airflow's own API, not warehouse admin rights.
Validation is the layer teams skip, and it is the one that stops silent damage. A task going green only proves the code finished. After recovery, check what the data consumers care about: row count against the trailing average, freshness of the target table, and null rates on key columns. If validation fails, the system must not retry again. It stops, reverts to the pre-recovery state where possible, and escalates with the full trail attached.
Record every step as a structured event: the original failure, the classification and confidence, the policy rule matched, the action taken, and the validation result. This gives you a postmortem for free and a dataset for improving classification. Our Pipeline Agent, available through Data Workers, follows this pattern of read-only diagnosis first and gated actions second, and it can be wired to the Incidents Agent for the classification step.
Where Humans Still Approve
Approval should be driven by risk, not by habit. A good rule of thumb: auto-approve actions that are idempotent, bounded, and reversible, such as retrying a task that reads from a source. Require a person for anything that writes to shared tables outside the failed run's own partition, changes schema or configuration, spans a wide time range, or touches regulated data. Schema drift is the clearest example: the agent can show the diff and blast radius, but a person decides whether to accept it.
Make approval cheap. An approver should see one message with the failure, the proposed action, the rule that allows it, and the evidence, with approve and reject buttons. If approving takes ten minutes of digging, people will rubber-stamp or ignore it, and the gate becomes decoration. Include an expiry so stale proposals do not execute hours later against changed data.
Also decide what the agent may do when nobody answers. The safe default is to leave the Dag in its failed state and keep the page active. An agent that acts because no one said no has turned an approval gate into a timer. The Model Context Protocol specification's guidance on tool safety also puts a human in the loop for sensitive tool invocations, which matches this posture.
A Rollout Plan You Can Defend
These are suggestions, not measured requirements. Start in propose-only mode, where the agent classifies and writes what it would have done but executes nothing. Replay a set of historical failures through the classifier and compare its labels against what your engineers actually did. The right size of that set and the right length of the propose-only period depend on your failure volume, so choose them from your own incident history rather than from a fixed number.
Then enable one failure class at a time, starting with the least risky, usually transient network errors on read-only tasks. Watch three signals: how often the policy blocks a proposed action, how often validation catches a bad recovery, and how often an engineer overrides the agent. A rising override rate is a stronger warning than any dashboard of successes.
Finally, keep a kill switch that turns every rule to page-only without a deploy, and test it. Self-healing earns trust by being boring, auditable, and easy to turn off. Source code and community discussion for the orchestrator itself live in the Apache Airflow repository, which is worth watching for changes to callback behavior between versions.
Frequently Asked Questions
Can an AI agent safely fix Airflow failures without human approval?
Yes, for a narrow set of bounded, idempotent, reversible actions, and only through a policy table that names each allowed action. Retrying a task after a transient network error is a typical example. Anything that changes schema, writes outside the failed run's partition, or touches regulated data should require a person. The Astronomer episode is explicit that AI does not bypass policy or approval steps.
Should the LLM decide which recovery action to run?
No. Let the model classify the failure into a closed list of classes and suggest context. Let a versioned policy table map each class to one allowed action. This keeps the decision auditable, lets you test the policy without calling a model, and prevents a confident but wrong diagnosis from turning into an arbitrary command.
Does on_failure_callback catch every failure?
Not every one. The Airflow documentation states callbacks run when a worker executes and changes state, so changes made through the CLI or UI do not trigger them, and callback errors surface in dag processor logs. Add a heartbeat that confirms failure events reach your classifier, and treat a silent observer as an incident of its own.