- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Data
Write and Run a Dag with the Task SDK in Apache Airflow 3
Airflow 3 changed how you write a Dag. The Task SDK gives you a clean, stable authoring interface, from airflow.sdk import dag, task, that keeps your pipeline code out of Airflow's internals and lets you run a whole Dag in one Python process with dag.test(), no scheduler required. In this lab you take a nightly orders-summary script that Globomantics runs by hand and turn it into a real Airflow 3 pipeline. You will define the Dag with the @dag decorator, break the script into @task functions that pass data through XCom just by returning it, wire the dependencies by calling the tasks like ordinary functions, and then run and verify the whole thing locally with dag.test(), including a parameterized run for a single sales region. You leave with a Dag you could drop into an Airflow 3 deployment and a fast local loop for testing the next one.
Lab Info
Table of Contents
-
Challenge
From a script to a Dag
Welcome to this lab on writing and running a Dag with the Apache Airflow 3 Task SDK. You play a data engineer at Globomantics. Every morning someone on the operations team runs a Python script by hand. It reads the latest orders export, keeps the orders that shipped, totals revenue per product, and writes a JSON summary that feeds a dashboard. The script works until someone forgets to run it, runs it twice, or runs it against the wrong region. The team is moving to Apache Airflow 3, and this script is the first pipeline to migrate.
What the Task SDK gives you
Airflow 3 introduced the Task SDK, a stable authoring interface that you import from
airflow.sdk. It keeps your pipeline code separate from the scheduler internals. You define a Dag by decorating a Python function with@dag, you turn ordinary functions into tasks with@task, and Airflow moves data between tasks through XCom whenever a task returns a value. You declare dependencies by calling one task with the result of another, the same way you call functions.Airflow 3 spells it Dag in its own documentation and release notes, so this lab does too.
Running a Dag without a scheduler
dag.test()runs a whole Dag inside one Python process. No scheduler, no API server, and no web UI are involved. That makes it the fastest loop for developing a Dag: edit the file, run it, read the output, repeat. In this lab every run happens throughdag.test(). The lab environment already initialized the local SQLite metadata database it needs.What you will build
You start with
dags/orders_pipeline.py, which holds the imports, two path constants, and an emptyorders_pipeline()function that wraps three empty nested functions with TODO comments. Across the next three steps you turn that file into a working three-task Dag:- Step 2 defines the Dag with
@dagand its metadata. - Step 3 implements
extract,transform, andloadas@taskfunctions and wires them together. - Step 4 runs the Dag with
dag.test(), passes in a region, and verifies the result from the returned DagRun.
The data lives in
data/orders.csv, 309 synthetic orders across four regions and four SKUs. Theloadtask writes its summaries tooutputs/. Each task ends with a check. Click Validate on the task to run it, and read the feedback if it fails.Note: If you get stuck at any point, the
solutions/folder contains the completedorders_pipeline.pyfor every task, and comments in each file name the task that adds each part. - Step 2 defines the Dag with
-
Challenge
Define a Dag with the @dag decorator
In the Task SDK a Dag is a decorated Python function. The
@dagdecorator carries the metadata that Airflow needs: a uniquedag_id, aschedule, astart_date, and whether Airflow should backfill missed runs withcatchup. Calling the decorated function once at module level produces the Dag object, and that is whatdag.test()runs later.This Dag has no schedule, because in this lab you always run it by hand. You still set
catchup=False. It has no effect whilescheduleisNone, but it is the habit to keep, because the moment you give a Dag a schedule,catchupdecides whether Airflow runs every missed interval sincestart_date. Theparamsyou just declared are the hook for Step 4. A run can override any param throughrun_conf, and every task can read the merged values. That is how the same Dag will summarize all regions on one run and a single region on the next. -
Challenge
Create tasks with the @task decorator
A
@taskfunction looks like a normal Python function, and that is the point. Whatever it returns becomes its XCom value, and whatever you pass into it from another task becomes a dependency. You never writeset_upstreamor>>for this pipeline. You writesummary = transform(rows)and Airflow knows thattransformruns afterextract.One detail matters here. A decorated function does not become a task until you call it inside the Dag function. Decorating
extractis not enough. The linerows = extract()insideorders_pipeline()is what registers the task on the Dag, so each task in this step adds both the decorator and the call.Note: XCom is for small handoffs. The 309 rows in this lab are about 15 KB of JSON, which is fine. For a real extract you would return a file path or an Asset and let the next task read the data itself. Declaring
paramsas a named argument is how the Task SDK hands a task its run context. Airflow inspects the function signature and injects the matching context values. You could also callget_current_context()fromairflow.sdk, but a named argument keeps the function easy to call and easy to test on its own, which is exactly what the check does.retriesis operator configuration, so it lives on the decorator rather than inside the function. Airflow will rerun a failedloadup to two more times in a scheduled deployment. In this lab the write succeeds on the first try, so you will not see a retry, but the setting travels with the Dag when it moves to production. -
Challenge
Run and verify the Dag with dag.test()
You now have a complete Dag, but nothing has run it.
dag.test()executes every task in dependency order inside the current Python process and returns a DagRun object. In this step you add the entry point that calls it, pass a region into a run, and then turn "it looks like it worked" into an assertion. The output is verbose because Airflow logs every state transition. Two lines matter. Each task reportsTask instance state updatedwithnew_state=success, and the run ends withDagRun Finishedandstate=success. Theprintcalls inside your tasks appear between them.run_confmerges into the Dag'sparamsfor that run only. The default ofallthat you declared in Step 2 still applies whenever no region is passed, which is why the earliersummary_all.jsoncame out the way it did. Compare states with the enums, not withstr(state).TaskInstanceStateandDagRunStateare string enums, sostate == TaskInstanceState.SUCCESSis true whether Airflow hands you the enum or the plain stringsuccess.str(TaskInstanceState.SUCCESS)is the textTaskInstanceState.SUCCESS, so a helper built onstr(state) == "success"fails the moment a run object carries the enum, and Airflow 3.3 already returns the enum fordag_run.state. The helper you just wrote is small, and it is the difference between a run you watched and a run you verified. -
Challenge
Conclusion and next steps
You rebuilt a script that someone ran by hand as an Airflow 3 Dag with the Task SDK. You defined the Dag with
@dagand gave it an id, a start date, tags, params, and docs. You created three tasks with@task, passed data between them through XCom by returning values, declared the dependencies by calling the tasks, and put a retry policy on the step that writes to disk. Then you ran the Dag withdag.test(), parameterized a run withrun_conf, and verified the outcome from the DagRun that came back, using the state enums that Airflow itself uses.Moving the Dag to a deployment
Three things change when this file lands in a real Airflow 3 environment. The
schedulebecomes a cron string or a timetable instead ofNone, andcatchupstarts to matter. The file lives in the deployment's Dag bundle, where the scheduler parses it. Theretriesonloaddo real work when a disk or network hiccup fails a write. Nothing about the tasks, the XCom handoffs, or the dependencies has to change.Where to go next
- Give the Dag a daily schedule, set
catchup=True, and watch Airflow create a backfill run for every day sincestart_date. - Replace the single
transformwith dynamic task mapping over the four regions so each region becomes its own task instance. - Return an Asset from
loadso a downstream Dag can run whenever a new summary lands. - Keep
dag.test()andverify_runin your workflow. They give you a local loop that takes seconds and catches most mistakes before the scheduler ever sees the file.
- Give the Dag a daily schedule, set
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.