People reach for Airflow when they actually need Temporal, and reach for Temporal when a cron job and a retry decorator would have done the job. The confusion is understandable — both call themselves "workflow orchestrators," both have a Python SDK, both draw a DAG-shaped picture in a UI. But they were built to solve different problems, and the difference shows up the moment your workflow needs to wait three days for a human to approve something, or a payment provider's webhook to arrive, rather than just running a fixed sequence of tasks on a schedule.
What Airflow actually is
Airflow is a scheduler for DAGs of tasks. You define a directed acyclic graph in Python — this task runs, then these two run in parallel, then this one runs after both finish — and Airflow's scheduler decides when to trigger a run, hands each task to an executor (Celery, Kubernetes, local), and tracks success or failure per task. The DAG itself is metadata: Airflow parses your Python file, builds the graph, and persists task state in its own database. It does not run your business logic in-process; each task is typically a self-contained unit — a SQL query, a Spark job submission, a call to an external API — that either succeeds or fails and can be retried.
This model is a great fit for batch and ETL work: pull yesterday's orders from Postgres, transform them, load them into a warehouse, then kick off three downstream reports. Everything is triggered on a schedule (or by a sensor watching for new data), every task is idempotent-ish and retryable, and a failed run just means re-running from the failed task the next day.
from airflow.decorators import dag, task
from datetime import datetime
@dag(schedule="@daily", start_date=datetime(2026, 1, 1), catchup=False)
def daily_order_etl():
@task
def extract():
return fetch_orders_since_yesterday()
@task
def transform(orders):
return [normalize(o) for o in orders]
@task
def load(rows):
write_to_warehouse(rows)
load(transform(extract()))
daily_order_etl()
What Temporal actually is
Temporal solves a different problem: how do you write code that keeps running correctly across days or weeks, through process crashes, deploys, and network failures, without the code itself turning into a state machine with a database table for every step? The mechanism is event sourcing and replay. A Temporal workflow is ordinary code — a function in Go, TypeScript, Python, or Java — but every meaningful action it takes (calling an activity, starting a timer, waiting on a signal) is recorded as an event in Temporal's server-side history. If the worker process running your workflow crashes, a new worker picks up the history and replays it deterministically to reconstruct exactly where execution had gotten to, then continues from that point. Your workflow function can call sleep(72 hours) waiting for an approval, and that's just another line of code — no cron job, no polling table, no "resume from step 4" logic you wrote by hand.
Business logic — order fulfillment sagas, multi-step approval chains, "reserve inventory, charge the card, and roll both back if either fails" — tends to fit this model far better than a DAG does, because the actual control flow has loops, conditionals, and waits for external events, not just a fixed graph of tasks.
import { proxyActivities, defineSignal, setHandler, condition } from '@temporalio/workflow';
import type * as activities from './activities';
const { reserveInventory, chargeCard, releaseInventory } =
proxyActivities<typeof activities>({ startToCloseTimeout: '1 minute' });
export const approvalSignal = defineSignal<[boolean]>('approval');
export async function orderWorkflow(orderId: string): Promise<string> {
let approved = false;
setHandler(approvalSignal, (value: boolean) => { approved = value; });
await reserveInventory(orderId);
// waits however long it takes — hours or days — with no polling loop
await condition(() => approved, '3 days');
if (!approved) {
await releaseInventory(orderId);
return 'cancelled: not approved in time';
}
await chargeCard(orderId);
return 'completed';
}
Scheduler + executor vs. event-sourced replay
The architectural split explains most of the practical differences. Airflow's scheduler wakes up, looks at DAG definitions and a metadata database, and decides what to run next — the unit of durability is the task, tracked as a row in a table. If a task fails, Airflow retries that task; it has no concept of "workflow state" beyond which tasks in the DAG have run. Temporal's unit of durability is the entire workflow execution — every activity call, timer, and signal is an event in a history that belongs to a single running instance, identified by a workflow ID. That's what lets a Temporal workflow hold local variables, loop, and branch across days of wall-clock time and still recover deterministically after a crash — there's no equivalent in Airflow, because Airflow tasks don't share process state across a graph the way a Temporal workflow function shares state across its own execution.
If step 5 needs to know a value computed in step 1 without re-reading it from a database, and the workflow might be paused for hours between the two, you want Temporal's replay model. If every step can be independently re-run against fresh data with no memory of prior steps, a DAG scheduler is simpler and sufficient.
Operational differences that matter day to day
Airflow gives you a mature UI for DAG runs, built-in scheduling, backfills, and a large ecosystem of provider packages for talking to warehouses, cloud services, and databases — if your problem is "move and transform data on a schedule," that ecosystem is worth a lot and you'd be reinventing it in Temporal for no benefit. Temporal gives you workflow versioning (so you can deploy new code without breaking workflows that are already mid-execution), child workflows, signals for external events, and query handlers to inspect a running workflow's state — none of which map cleanly onto Airflow's task-graph model, because Airflow simply wasn't designed for workflows that live for days and react to external events out of band.
I've seen teams simulate "wait for approval" in Airflow with a sensor task that polls a database every few minutes for days. It works, but it's fighting the tool — you're building your own event-sourcing layer on top of a scheduler that wasn't designed for it. That's usually the sign the workflow belongs in Temporal, not that Airflow needs a cleverer sensor.
When each one fits
Reach for Airflow when the work is fundamentally batch: a fixed set of tasks that runs on a schedule or in response to new data landing somewhere, where each task is independently retryable and the whole thing is done within minutes or hours. ETL, model training pipelines, nightly report generation, and data warehouse loads are the canonical cases — the DAG really is the shape of the problem.
Reach for Temporal when the work is a long-running business process with real control flow: order sagas that span multiple services and need compensation logic if something fails partway through, approval workflows that wait on a human for an unpredictable amount of time, or any process where you'd otherwise be building your own "resume from where we left off" logic on top of a database table. The tell is usually the word "wait" — waiting on a person, another system, or a fixed delay measured in hours or days, in the middle of otherwise ordinary application logic.
Wrapping up
Airflow orchestrates data; Temporal orchestrates business processes. Airflow assumes each run is a fresh pass over a fixed graph of tasks with no shared in-process state; Temporal assumes a single logical execution can live for days, hold state, and react to events, and gives you replay-based durability so you don't have to build that by hand. Neither is a general replacement for the other — using Temporal for nightly ETL is over-engineering, and using Airflow for a multi-day approval workflow means reinventing event sourcing badly, one sensor task at a time. Pick based on whether what you're modeling is a data pipeline or a process with memory.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.