Airflow Tutorial
Apache Airflow for Beginners (Airflow 3)
Apache Airflow is an open source platform for writing and monitoring pipelines as Python code, with scheduling built in. This tutorial for beginners installs it locally and writes a first DAG with operators and with the TaskFlow API. It also explains how schedules and retries work, and what XComs are.
What is Apache Airflow?
Apache Airflow is an open source workflow orchestrator: you describe a pipeline as Python code, and Airflow runs its steps in order on a schedule and keeps a record of every run. Failed steps are retried. A pipeline is a DAG (directed acyclic graph) of tasks. This tutorial installs the software on your machine, writes a first DAG 2 ways and explains the behavior that surprises beginners: data intervals and catchup, plus retries and XComs.
Airflow does not move or transform data itself. It decides when a task starts and what must have succeeded first, and the task calls the system that does the work, such as a SQL engine or a Spark job. That separation is why the same tool schedules very different pipelines, and why the best DAGs are short.
Everything below follows the Apache Airflow documentation, whose stable pages showed version 3.3.2 when this page was written. The imports use the airflow.sdk package that the current tutorials use.
How to install and run Airflow locally
- 01
Check your Python version
The quick start lists Python 3.10 through 3.14 for Airflow 3.2.0 and later. It supports pip and uv as installers. Windows users need WSL2.
- 02
Install with a constraint file
Create a virtual environment, then install apache-airflow pinned to a version together with the constraint file for that Airflow version and your Python version. The constraints keep every dependency at a combination the project tested.
- 03
Start every component with one command
Run airflow standalone. It initializes the database and creates a user, then starts all components. The first run also creates AIRFLOW_HOME, which defaults to ~/airflow, and a default airflow.cfg.
- 04
Open the web interface and sign in
Browse to localhost:8080. The generated admin password is stored in simple_auth_manager_passwords.json.generated inside AIRFLOW_HOME, so it is not always printed to the terminal.
- 05
Put a DAG file in the dags folder
Airflow reads the folder named by dags_folder in the core section of its configuration. Save a Python file there and the DAG appears in the interface after it is parsed.
How to write your first Airflow DAG
A DAG file has 3 parts: the imports, the DAG object that carries the schedule, and the tasks with the dependencies between them. The dag_id is the name you see in the interface. schedule says when runs are created. start_date is the earliest point a schedule can use, and catchup=False stops Airflow from creating a run for every missed interval since that date.
Each task is an instance of an operator, a predefined template for one kind of work. The BashOperator runs a shell command, and its command string is a Jinja template, so {{ ds }} renders as the run's logical date in YYYY-MM-DD form. The >> operator sets order: extract >> transform >> load means transform starts only after extract succeeds. With no other setting, a task runs when all of its upstream tasks have succeeded.
The fan-out form works too. The Airflow fundamentals tutorial writes t1 >> [t2, t3] to run t2 and t3 in parallel after t1. Save the file in the dags folder and run it once with python to confirm it parses: an import error or a syntax error shows up immediately, before the scheduler ever sees the file.
The same DAG with operators and with the TaskFlow API
from datetime import datetime
from airflow.providers.standard.operators.bash import BashOperator
from airflow.sdk import DAG
with DAG(
dag_id="orders_daily",
schedule="@daily",
start_date=datetime(2026, 1, 1),
catchup=False,
) as dag:
extract = BashOperator(
task_id="extract",
bash_command="echo extracting orders for {{ ds }}",
)
transform = BashOperator(
task_id="transform",
bash_command="echo transforming orders",
)
load = BashOperator(
task_id="load",
bash_command="echo loading orders",
)
extract >> transform >> load
import pendulum
from airflow.sdk import dag, task
@dag(
schedule="@daily",
start_date=pendulum.datetime(2026, 1, 1, tz="UTC"),
catchup=False,
)
def orders_daily():
@task
def extract():
return [120.0, 80.5, 99.5]
@task
def transform(amounts: list[float]) -> float:
return sum(amounts)
@task
def load(total: float):
print(f"Total order value: {total:.2f}")
load(transform(extract()))
orders_daily()
Operators vs the TaskFlow API
The TaskFlow API turns a Python function into a task with the @task decorator, and a @dag function into the DAG. You import dag and task from airflow.sdk. Dependencies come from the calls themselves: writing transform(extract()) makes transform wait for extract, so most TaskFlow DAGs need no >> at all.
The values pass between tasks too. The documentation states that a task's return value is stored as an XCom and handed to the downstream task automatically. With multiple_outputs=True, a returned dictionary is split into one XCom per key, so a later task can take result["total_order_value"] alone.
Choose by the work. A task that runs a shell command or calls a provider's operator is clearest as an operator. A task that is a few lines of Python is clearest as a decorated function. Both styles live in one DAG, and TaskFlow tasks can depend on operator tasks.
Know Apache Airflow the way the interviewer who asks it knows it.
Schedules, logical dates and the data interval
The schedule argument takes a cron expression, a timedelta, a preset such as @daily, or None for a DAG that only runs when something triggers it. Each scheduled point creates a DAG run, a record in the metadata database, and each run has a data interval: the span of time it processes.
A @daily schedule, equal to the cron expression 0 0 * * *, gives each run a 24 hour interval from midnight to midnight. The logical date, called execution_date before Airflow 2.2, marks the start of that interval. The documentation is explicit that it denotes the start of the data interval and not when the DAG is executed: the run for a given day does not start until that day has ended.


This is the rule that confuses beginners. A daily DAG that processes yesterday's orders runs in the early hours of today, and its logical date is yesterday. Read data_interval_start and data_interval_end inside a task to select exactly the rows the run owns, instead of reading whatever is latest. For a manual trigger in Airflow 3, the documentation warns against assuming the run's data interval equals the supplied logical date.
Catchup vs backfill: what happens to missed intervals
When a DAG is first switched on, the scheduler creates a run only for the latest interval, because the scheduler.catchup_by_default setting is False. Set catchup=True on the DAG and the scheduler creates a run for every interval since the last one that ran, which also happens when you pause a DAG for a period and switch it back on.
A backfill is the deliberate version: you choose a historical period and Airflow re-runs the DAG over it. The command is airflow backfill create, which takes a from date and a to date. Further options set reprocess behavior and maximum active runs, or run the period backwards. The Trigger menu on the DAG details page opens a backfill form in the interface too.
Both features run your code many times over old intervals, so they only work safely when a task can run twice with the same outcome. The idempotency guide covers how to design for that.
The components of an Airflow deployment
Triggers scheduled workflows and submits tasks to the executor
Parses DAG files from a bundle and serializes them into the metadata database
Serves the REST API and the interface where you inspect and debug DAGs and tasks, or trigger them
Execute the tasks the scheduler gives them, each in a new subprocess
Optional: runs deferred tasks in an asyncio event loop
Usually PostgreSQL or MySQL: stores the state of tasks and DAGs
How a task runs, from file to finished
The DAG processor parses each file in the dags folder and writes the result to the metadata database. The scheduler reads those records, creates DAG runs on schedule and submits ready tasks to the executor. The executor is a configuration property of the scheduler and runs inside the scheduler process, so it's a setting, not a separate service.
A task instance moves through states as this happens. The documentation lists 14, among them scheduled and queued, then running, success, failed, up_for_retry and skipped, and finally upstream_failed. The last one tells you a task never ran because something it depended on failed, which is where to look for the real error.
Because the dag processor re-parses files repeatedly, a DAG file is not the place for heavy work. The documentation says not to run heavy computation or database and network calls at the top level of a DAG file, since that code runs on every parse and slows the scheduler. Put the work inside tasks.
How tasks pass data: XComs and their limit
Tasks are isolated, each in its own process and often on its own machine. An XCom, short for cross-communication, is the mechanism that lets them talk: a value stored against a key, scoped by task ID and DAG ID, readable by later tasks in the same run. TaskFlow return values use it implicitly. Traditional operators call xcom_pull() explicitly.
The documentation says XComs are designed for small amounts of data and warns against passing large values such as dataframes. Pass a reference instead: write the large result to storage and return its path or table name, so the next task reads from there. For larger payloads, the documentation points to the object storage XCom backend, and the backend is swappable through the xcom_backend configuration option.
Retries and idempotent tasks
Retries are set per task or in the DAG's default_args. The same tutorial sets retries to 1 with a retry_delay of 5 minutes, and overrides it to 3 retries on one task. A task waiting for its next attempt shows the up_for_retry state.
Retries make one rule non-negotiable. The documentation puts it directly: Airflow can retry a task if it fails, so the task should produce the same outcome on every re-run. It recommends UPSERT in place of INSERT, since an insert can duplicate rows, and reading the partition for data_interval_start instead of the latest available data. A task written this way survives retries, and the re-runs that catchup and backfill cause, without cleanup. The idempotency guide works through the patterns.
Airflow command line cheat sheet
airflow standaloneairflow db migrateairflow dags listairflow tasks list <dag_id>airflow dags show <dag_id>airflow tasks test <dag_id> <task_id> <date>airflow dags test <dag_id> -f <file>airflow backfill createHow to test a DAG before you schedule it
Test in 3 widening steps. First run the file with python so imports and syntax fail loudly. Then check what Airflow sees with airflow dags list and airflow tasks list. Finally exercise real work: airflow tasks test runs one task instance for a logical date, and airflow dags test runs a whole DAG run locally and records its state in the metadata database.
The documentation describes dag.test() as an integration test that needs a metadata database, so it complements unit tests of your task functions and does not replace them. Keep the logic of a TaskFlow task in plain functions you can call from a test with ordinary arguments.
Where to go after your first DAG
Interviewers use Airflow to probe whether you can reason about pipeline behavior: what a retry does to a non-idempotent task, what a backfill touches, why a daily run starts after midnight. The data pipeline interview questions and data pipeline practice problems put those ideas under time pressure, and the Airflow interview questions go deep on the tool itself.
The Airflow DAG guide covers the DAG object and its runs in more detail. When a design question asks how you would operate a pipeline, the system design round is the format to rehearse.
Airflow tutorial FAQ
What is Apache Airflow used for?+
Is Apache Airflow hard to learn for beginners?+
Do I need to know Python to learn Airflow?+
What is a DAG in Airflow?+
What is the difference between a logical date and the date a run starts?+
How do I run Airflow on my own machine?+
Should I use Airflow standalone in production?+
Is Airflow a data transformation tool?+
The candidate who gets the offer
- 01
Reading a solution is not the same as writing one
Every engineer who has frozen on a query they had read a dozen times knows the gap. The only preparation that closes it is producing the answer yourself, under time, before the interview does it for you
- 02
76% of hiring managers reject on the coding task, not the resume
From HackerRank's 2024 Developer Skills Report. Candidates who look strong on paper still fail the live screen if they haven't done timed, executable practice
- 03
System design comes down to the calls you defend out loud
Ingestion, batch vs streaming, the bronze/silver/gold layers, idempotency, backfill and replay. Sketching the pipeline and naming the failure modes is the signal, not the boxes
Related guides
Airflow DAG
What a DAG is, how it runs, with examples
Idempotency in data engineering
Tasks that are safe to run twice
Airflow schedule interval
How schedules, data intervals and logical dates fit together
Data pipeline interview questions
Pipeline design and operations questions
Data pipeline practice problems
Design problems to practice before the loop
Airflow interview questions
Questions on DAGs and operators, and on how scheduling works
Compete in the Weekly
Dirty, production-shaped data, scored blind each week.