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.

Last updated: Proudly published by: Jeff Wahl9 min read

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

PYorders_daily.py
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

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.

Prepare for the interview
01 / Open invite
02min.

Know Apache Airflow the way the interviewer who asks it knows it.

a Apache Airflow query, the same shape a screen would give you.
The diff against expected. Where ties broke. What you missed.
sandbox
1source → bronze → silver → gold
2 ingest : CDC + Kafka
3 transform : dbt + Airflow
4 serve : Snowflake
5
Execute your solution0.4s avg.
NetflixInterview question
Solve a Apache Airflow problem

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.

Timeline of one @daily Dag run: the logical date marks the start of a 24 hour data interval from 00:00, and the run starts at 24:00, when the interval has ended and the next interval beginsTimeline of one @daily Dag run: the logical date marks the start of a 24 hour data interval from 00:00, and the run starts at 24:00, when the interval has ended and the next interval begins

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

Scheduler
Who starts tasks?

Triggers scheduled workflows and submits tasks to the executor

Dag processor
Who reads the files?

Parses DAG files from a bundle and serializes them into the metadata database

API server
What serves the UI?

Serves the REST API and the interface where you inspect and debug DAGs and tasks, or trigger them

Workers
Who runs the code?

Execute the tasks the scheduler gives them, each in a new subprocess

Triggerer
Who waits cheaply?

Optional: runs deferred tasks in an asyncio event loop

Metadata database
Where is state kept?

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 standalone
Initializes the database and creates a user, then starts every component for local use
airflow db migrate
Creates or upgrades the metadata database schema
airflow dags list
Lists the DAGs Airflow has found
airflow tasks list <dag_id>
Lists the tasks of one DAG
airflow dags show <dag_id>
Renders the DAG's task graph
airflow tasks test <dag_id> <task_id> <date>
Runs one task instance for a logical date
airflow dags test <dag_id> -f <file>
Runs one whole DAG run locally from a file
airflow backfill create
Re-runs a DAG over a chosen historical period

How 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?+
Apache Airflow is an open source platform for writing and monitoring workflows as Python code, with scheduling built in. Data engineers use it for pipelines whose steps must happen in a set order. Airflow runs them on a schedule, keeps a record of every run, and retries failed steps.
Is Apache Airflow hard to learn for beginners?+
The first DAG is small: a few tasks and the `>>` operator. The harder ideas are the data interval, which decides what a run covers, and writing tasks that can safely run twice. Both take a few hours of practice rather than weeks.
Do I need to know Python to learn Airflow?+
Yes. DAG files are Python, and the TaskFlow API turns ordinary Python functions into tasks. Comfort with functions and decorators is enough to start.
What is a DAG in Airflow?+
A DAG, written Dag in the current documentation, is a directed acyclic graph of tasks: each task is a node, each dependency is an edge pointing from a task to the one that waits for it, and no path loops back on itself.
What is the difference between a logical date and the date a run starts?+
The logical date marks the start of the run's data interval. For a @daily schedule, the run for the interval beginning at 00:00 does not start until that interval ends at 24:00.
How do I run Airflow on my own machine?+
Install `apache-airflow` with the constraint file for your Python version, then run `airflow standalone`. It initializes the database and creates a user, then starts every component, and the web interface is at localhost:8080.
Should I use Airflow standalone in production?+
No. The quick start describes standalone as a local setup and says that in production the components should run separately, following the Production Deployment guide.
Is Airflow a data transformation tool?+
Airflow orchestrates: it decides what runs when, and in what order. The work itself runs in the system the task calls, such as a SQL transformation or a Spark job.
02 / Why practice

The candidate who gets the offer

  1. 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

  2. 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

  3. 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