Apache Airflow event-driven scheduling with asset watchers

Event-driven scheduling starts Airflow 3 DAGs from messages instead of clocks. This page covers how AssetWatcher and BaseEventTrigger work, what to design for idempotent consumers and acknowledgements, and what to confirm before production.

On this page

When Apache Airflow event-driven scheduling replaces clock-based DAG runs

An AssetWatcher updates an Airflow asset whenever an event trigger fires, so a DAG that consumes that asset can start when a message arrives instead of when a cron expression says it should. Teams need this when a pipeline depends on an upstream system that publishes when its data is ready, and a fixed schedule either starts too early and waits on nothing, or starts late after the data has already landed. It also helps when a clock-based schedule reruns a pipeline on days when no new data exists, which wastes compute and can overwrite downstream tables with identical results.

The mechanism is narrower than a general webhook. Only triggers that inherit from BaseEventTrigger are compatible with an AssetWatcher, so a custom trigger or a provider trigger that does not follow that base class cannot be wired in. The documented message broker sources are Kafka, Amazon SQS, Google Cloud Pub/Sub and Azure Service Bus. Before you design around any of them, confirm in the provider package version you plan to install that the trigger you need exists and inherits from that base class, because the list of supported sources can change between releases. AssetWatcher was added in Airflow 3.0, and the current patch release is 3.3.2, so teams on Airflow 2 cannot use this feature without first moving to Airflow 3.

Confirm two things early: which team owns the message source and the consumer group or queue that Airflow will read, and where the Triggerer runs in your deployment, since event triggers depend on it. Event-driven scheduling can deliver duplicate events after a Triggerer restart but does not lose them, so the design has to assume repeats from the first day. Decide whether each asset update should start one run, batch several messages, or only signal that a partition is ready. Those choices determine how much of the consumer logic lives in the trigger and how much lives in the DAG.

Choosing a message source and its trigger

The event-driven scheduling docs name Kafka as a supported message source. Most teams that use Kafka already run it as a shared streaming platform, so the trigger decision starts with topic ownership and consumer design, not with the DAG. Settle those questions before you write any AssetWatcher code, because a trigger that joins the wrong consumer group can affect other teams.

  • Consumer group and offsets: Decide which consumer group the trigger uses and who owns its offsets, so a DAG change does not reset the replay position for other consumers.
  • Auto-commit disabled: Kafka consumers must disable auto-commit, so an offset advances only after the event has been handled.
  • Duplicates after restart: A Triggerer restart can redeliver events, so the downstream DAG must be idempotent.
  • Network and TLS: Confirm that the triggerer can reach every bootstrap broker address and that TLS certificates and SASL credentials are issued for the triggerer's identity.
  • Provider package: Confirm which Kafka provider package your Airflow 3 image pins, and test it against your cluster version before production.

Before the first build, ask your streaming team which topics, partitions and ACLs the triggerer identity needs, and confirm them in writing. Read the event-driven scheduling documentation for the current trigger requirements.

Configuration options and when each fits

This table compares the design choices that shape how an event-driven DAG behaves, from the trigger type to how many watchers a consumer uses. Read each row as a decision: the right column names the condition under which the option makes sense and the cost it carries. The main takeaway is that the acknowledgement and auto-commit settings matter as much as the trigger choice for avoiding duplicate or lost work.

DecisionWhen it fitsTrade-off to review
Time-based scheduleWork that must run at a fixed time regardless of when data arrives, such as a nightly reconciliationRuns wait for the clock even when the input is ready, and late data needs a separate catch-up plan
AssetWatcher with a custom BaseEventTriggerWork that should start when a message lands on a broker such as Kafka, SQS, Pub/Sub or Azure Service BusYour team owns the trigger code, its error handling and its tests, and the trigger must inherit from BaseEventTrigger
Asset condition with ANDA DAG that should run only after every upstream asset has updated, such as a join across two feedsA slow or silent source blocks the whole run, so confirm how long each input can be late
Asset condition with ORA DAG that should run on the first available update from any of several sourcesMultiple sources can trigger the same work repeatedly, so check that the run is safe to repeat
Single watcher per consumerOne event stream feeds one DAG with a clear owner and a small set of message typesOne trigger failure affects only that DAG, but the consumer group and offsets become harder to reason about across shared topics
Several watchers per consumerOne DAG reacts to events from different brokers or topics, such as a message plus a file dropMore moving parts to monitor, and each watcher needs its own acknowledgement and retry behavior reviewed
Default ack timeout of 5 minutesMessages whose processing finishes well within the default windowLonger tasks may see redelivery before they finish, so confirm the timeout against your slowest step
Kafka auto-commit disabledAny Kafka-backed watcher, because offsets must advance only after the event is handledDisabling auto-commit requires explicit commit logic, and the consumer must be tested for duplicates after restarts

Setting up an event-driven DAG step by step

This sequence takes a team from a supported Airflow 3 install to a consumer DAG that runs when a broker message arrives, with checks that confirm the triggerer, the asset and duplicate handling behave as expected before anything reaches production.

  1. Confirm the Airflow 3 version and prerequisites

    Your platform engineers check that the target Airflow release is a current 3.x patch, that the Python, metadata database and Kubernetes versions match the prerequisites page, and that the metadata database is PostgreSQL or MySQL rather than SQLite. Confirm the broker client libraries you need can be installed in the same environment. This step is complete when the version check passes in the environment you will actually run, and the team has written down which broker, topic or queue the DAG will consume.

  2. Run the triggerer with the scheduler and API server

    Platform engineers start the triggerer as its own process or container next to the scheduler and API server, since event triggers run there rather than on workers. Use the same metadata database and DAG delivery as the other components. The check is that the triggerer registers a heartbeat in the component list shown in the UI or health output, and that a simple deferred test task reaches the deferred state and resumes. Without a running triggerer, events are never picked up, so this check comes before any broker work.

  3. Write a BaseEventTrigger subclass for the broker

    A developer writes a trigger class that inherits from BaseEventTrigger and implements the async loop that reads from the broker, such as Kafka, SQS, Pub/Sub or Azure Service Bus, and yields a payload for each message. Keep the client setup inside the trigger, handle connection errors explicitly, and make the message-to-payload mapping small and testable. Consumer offset or acknowledgement behavior must match the broker's rules: for Kafka, auto-commit must be disabled. The check is a unit test that feeds sample messages into the trigger and confirms one payload is yielded per message.

  4. Define the asset with an AssetWatcher

    Next, the developer declares an asset with an AssetWatcher that points at the trigger class and names the event field the asset will carry. The watcher updates the asset when the trigger fires, so the asset URI should be stable and meaningful to your data owners. Keep the watcher definition in the same DAG bundle or shared module that the consumer DAG uses, so both sides agree on the name. The check is that the asset appears in the UI with the watcher attached and that a test message published to the broker results in an asset event being recorded.

  5. Write an idempotent consumer DAG

    A developer writes the DAG that is scheduled on the asset and makes every task safe to repeat. The docs state that events can be delivered twice after a triggerer restart but are never lost, so writes should use upserts, keys derived from the message identity, or a processed-message record checked before work begins. Read the event payload from the asset event rather than assuming a single message per run. The check is a DAG run triggered by a test event whose effects are identical when that same event is replayed manually.

  6. Test duplicate delivery and triggerer restarts

    The QA or platform team sends the same message twice and confirms the consumer produces one set of results, then restarts the triggerer while messages are in flight and checks that no event is missing from the asset history. They also confirm that the acknowledgement timeout, whose default is 5 minutes according to the docs, is longer than the time the consumer needs for a normal run, and that a failing run does not trigger repeated reruns with no change in input. Record the outcomes in the change ticket. This step is complete when duplicates produce no double writes and a restart produces no gaps.

  7. Watch for endless rerun conditions before release

    Before go-live, the team reviews the asset conditions and the consumer's failure behavior with an operator who owns the broker. Check that the DAG cannot be re-queued by its own output, that a poison message does not block the queue indefinitely, and that retry settings do not repeatedly consume the same event. Confirm with the broker owner how dead-letter handling works in their system. The check is a documented decision for each failure path and a dry run in a non-production environment that stops cleanly.

Why idempotent consumers are required, not optional

Event-driven scheduling can deliver the same event more than once, so every subscriber must tolerate repeats. The documented behavior is that a Triggerer restart can produce duplicate events, but it does not lose events. That guarantee is useful, but it shifts the burden to the consumer. A message that starts a DAG run, updates an asset, or writes a row may arrive again after a restart, and the pipeline has to produce the same result whether it sees the message once or twice. Treat idempotency as a design requirement for each AssetWatcher trigger, not as a hardening step to add later.

Common patterns are general, and the right one depends on the target system. A deduplication key taken from the message identifier, stored with the result and checked before processing, stops a second copy from being applied. Upserts keyed on a natural business identifier let a repeated load overwrite the same row instead of appending another. Another approach is to check whether the output for a given message already exists, such as a file path or partition written by an earlier run, and skip the write when it does. Non-idempotent loads, such as plain inserts into a fact table or sends to an external API without a request key, will double count or double act on a duplicate. Ask the data owners which writes can tolerate repetition and which need a guard.

Test restart behavior before production, because the duplicate window is a property of your deployment and not something to assume. Restart the Triggerer while messages are in flight in a non-production environment, then compare target tables, files and downstream calls against the source messages. Confirm how your broker's consumer settings interact with the trigger, including the requirement that Kafka consumers disable auto-commit and the default ack timeout of 5 minutes documented for event-driven scheduling. Record the results as evidence for the change review, and repeat the test after each Airflow upgrade.

Conditions that cause endless DAG reruns

Event-driven DAGs fire on messages, so any path that makes a run change the state the trigger watches, or that lets the same message come back, can restart the pipeline again and again. The cards below cover the four patterns that most often create these loops and the design check that catches each one before production.

DAG updates its own trigger asset

A DAG that writes to an asset which also feeds its own AssetWatcher will be started by its own output. Each run produces an event, the event starts the next run, and the cycle continues until someone pauses the DAG. Before release, map every asset each DAG produces and confirm that no producer chain leads back to a consumer in the same loop. Keep a separate asset for status or audit writes that no watcher subscribes to.

Trigger re-fires on unchanged state

A custom BaseEventTrigger that polls a source and emits an event on every poll, instead of only when the source changes, produces the same signal repeatedly. Downstream runs then look like new work even though nothing new arrived. Check that the trigger tracks the last position it emitted, such as an offset, a timestamp or a version, and emits only past that point. Test the trigger against a source that has not changed and confirm it stays quiet.

Acknowledgement timeouts cause redelivery

If a run takes longer than the broker's acknowledgement window, the broker assumes the message failed and delivers it again. The default ack timeout in Airflow event-driven scheduling is 5 minutes, so a consumer that holds a message while a slow task runs can trigger a second copy of the same work. Measure how long the consumer holds each message, acknowledge once the event is safely recorded, and make the downstream write idempotent so a redelivered message changes nothing.

Combined conditions match one event twice

When several assets feed one AND or OR condition, a single upstream event can satisfy the condition on more than one path, and each match can start a run. Repeated assets in a condition and overlapping producer paths make this worse. For each consumer, list the asset events that can arrive together and check how many runs they start. Where one business event should start one run, route it through a single asset and keep the condition as simple as the requirement allows.

Duplicate events after Triggerer restart

Airflow can deliver an event again after a Triggerer restarts, although it does not lose events. Any DAG that treats each event as a new unit of work, such as inserting rows or sending a notification, will repeat that work after a restart. Give each event a stable key, record processed keys in the target system, and skip keys already seen. Test this by restarting the Triggerer during a batch of events in a staging environment.

Questions teams ask before building event-driven DAGs

How much effort does event-driven scheduling take, and what drives it?

Effort depends mostly on the number of event sources, how much idempotency work each consumer needs, and how much time goes into testing restarts. Each source adds its own trigger configuration, broker credentials, message schema handling and failure cases. Idempotency is usually the largest single item, because Airflow can deliver a duplicate event after a Triggerer restart but does not lose events, so every downstream task must tolerate seeing the same message twice. Restart testing is the item teams most often underestimate. A fixed-scope quote should list each source, each consumer DAG, the duplicate-handling approach and the restart tests as separate line items, so you can see what each one costs.

Can our internal team build event-driven DAGs without a partner?

Yes, if the team already runs Airflow 3 in production and has engineers comfortable with message brokers and Python async code. The work requires knowing how AssetWatcher triggers inherit from BaseEventTrigger, how acknowledgement timeouts behave and how Kafka consumers handle offsets. Teams without broker experience usually benefit from a partner for the first source, then take over the pattern. Ask whether your engineers have built a trigger that consumes from your broker and survived a Triggerer restart in a test environment. If not, that gap is the part to plan for.

What access and network paths does the work need from us?

You will need to provide read access to the broker topics or queues the DAGs consume, a consumer group or subscription that is dedicated to Airflow, and network routes from the Triggerer to each broker. Credentials should come from your secrets store rather than DAG files. You will also need test messages on non-production topics so the team can verify triggers, duplicates and acknowledgement behavior before production traffic reaches them. Confirm with your broker administrators which ports, TLS settings and authentication methods apply in your environment, because these vary by platform and are not set by Airflow alone.

How long does it take to confirm event-driven scheduling works in our environment?

The duration depends on how many sources you have, whether the brokers are already reachable from the Triggerer, and how many test scenarios your team requires. No general timeline is given here. Ask the quote to split the schedule into a connectivity check, one trigger built against a test topic, a duplicate and restart test, and a production cutover with monitoring. Each stage should have an exit condition that your team signs off, so the confirmation point is written down rather than assumed.

Does event-driven scheduling replace our cron-based DAGs?

Not automatically. An asset watcher starts a DAG from a message, while a cron schedule starts it from the clock, so the right choice depends on whether the work should follow data arrival or a fixed calendar. Many teams keep cron schedules for reporting windows and move ingestion or reactive steps to events. Before changing a schedule, review how cron timetables interpret logical dates and catchup, because a switch can change run timing. List each existing DAG and decide, one by one, whether its trigger should stay on the clock or follow an asset.

What happens to duplicate events, and how do we handle them?

Airflow can deliver duplicate events after a Triggerer restart, but it does not lose them, so each subscriber has to be idempotent. In practice, that means a repeated message should produce the same result as the first, for example by writing with a unique key, using upserts, or recording processed message IDs before side effects run. Kafka consumers must disable auto-commit so that offsets are committed by the pipeline only after work completes. The default acknowledgement timeout is 5 minutes, so long-running steps need a plan for it. Test duplicates deliberately during the restart test rather than relying on production to reveal them.

What conditions cause a DAG to rerun endlessly, and how do we prevent it?

A loop usually appears when a DAG writes back to an asset it watches, or when a trigger keeps firing because a message is never acknowledged or the consumer fails before committing. Check that no task produces an event on the same asset it consumes, that failed messages are routed to a dead-letter path rather than replayed forever, and that acknowledgement timeouts are longer than your slowest step. Add alerts on run frequency for each asset so a loop is visible within hours rather than discovered in a cost review. These checks belong in the test plan before the first production trigger is enabled.

How DeployOpen works on this

DeployOpen plans, implements, hardens, documents and supports self-hosted Apache Airflow inside your own infrastructure. For event-driven scheduling, we start from the message sources, delivery guarantees and rerun behavior your pipelines must respect, then build asset watchers against them.

How we scope

  • Inventory every message source that should start a DAG, including broker type, topic or queue, and who owns it
  • Agree the idempotency design each downstream DAG needs, since duplicates can follow a Triggerer restart
  • Set the written scope, deliverables and timeline before work starts, as most engagements are fixed-scope

How we build

  • Implement AssetWatcher triggers that inherit from BaseEventTrigger for the broker types in scope
  • Configure consumer acknowledgement, auto-commit and ack timeout behavior to match the agreed delivery model
  • Test stop conditions and guard against endless reruns before any watcher runs against production traffic

How we hand over

  • Document each asset, watcher, trigger and consuming DAG, with the message contract and owner for each
  • Provide runbooks for triggerer failover, duplicate handling, stuck acknowledgements and replaying a message
  • Walk your platform and data teams through the configuration so they can change it after handover

How we support

  • Offer ongoing support, upgrades and testing as a retainer after the initial build is accepted
  • Review watcher behavior when Airflow patch releases or broker client libraries change in your environment
  • Confirm the broker features and Airflow version you run before scoping, since capabilities vary by release

Talk to our team.

Tell us what you're working on, whether it's a deployment, an audit, a security test or a cyber range. You'll speak with an engineer who can help you scope it.

  • 30-minute call: free, with no obligation.
  • NDA on request: we can sign before you share details.
  • Clear next steps: a scope and plan after the call.