โ† Back to playground

๐Ÿ”€Workflow Orchestration

Three tools answer to the word "workflow," and they are not the same machine. Temporal is a durable-execution engine you write as ordinary code; Apache Airflow is a Python DAG scheduler for data pipelines; AWS Step Functions is a managed JSON state machine wired into AWS. An exhaustive, primary-source comparison of how each one models work, survives failure, scales, deploys, and bills.

Temporal Apache Airflow AWS Step Functions

TL;DR

  • Temporal is a durable-execution engine. You write the workflow as a normal procedural function in your own language, and the engine makes that function itself crash-proof: it runs to completion exactly once, whether that takes a second or a year. Depth lives in the companion page on durable execution; this page places it next to the others.
  • Apache Airflow is a DAG scheduler. You author pipelines as Python that builds a graph of tasks, and a scheduler runs them โ€” classically on a time cadence โ€” with a vast operator ecosystem and a pipeline-ops UI. It re-runs tasks; it does not resume a function mid-line.
  • AWS Step Functions is a managed state machine. You describe the workflow as a JSON state machine in Amazon States Language, and AWS runs it for you โ€” no servers, wired directly into 200+ AWS services.
  • Same word, three machines. The deepest split is who owns durability and what the unit of work is: a durable function (Temporal), a scheduled task in a graph (Airflow), or a state in a managed state machine (Step Functions). Almost every other difference follows from that.

Three philosophies

Pick the right axis and the whole comparison snaps into focus. That axis is: what is the unit of work, who guarantees it survives failure, and how do you author it? Each tool answers differently, and almost everything else โ€” languages, scaling, pricing, lock-in โ€” is downstream of the answer.

  • Temporal โ€” the durable-execution engine. The unit is a workflow function that calls activities. Durability is owned by the engine: it records an event history and rebuilds in-memory state by replaying the log after any crash. You author in code (Go, Java, Python, TypeScript, .NET, PHP). The pitch: "write code as if failure doesn't exist."
  • Airflow โ€” the DAG scheduler. The unit is a task inside a directed acyclic graph. There is no replay and no resumed call stack: durability is the scheduler re-running tasks whose state lives in a metadata database, on the assumption that tasks are idempotent. You author in Python that declares the graph; the bodies do the work. It is built for batch pipelines on a schedule.
  • Step Functions โ€” the managed state machine. The unit is a state in a finite-state machine. Durability is owned by the managed AWS service, which persists every state transition for you. You author declaratively in Workflow Studio or raw ASL JSON; the real logic lives in the AWS services each state invokes.
Temporal durable-execution engine unit: a workflow function def run(): event log engine owns durability via history + replay "write code as if failure can't happen" Apache Airflow DAG scheduler unit: a task in a graph A B C D scheduler re-runs tasks state in a metadata DB โฑ runs on a schedule pipelines as Python Step Functions managed state machine unit: a state in ASL AWS-managed Task Choice Map service owns durability persists each transition JSON states ยท zero infra wired into AWS
The same word, three machines. The unit of work and the owner of durability differ in each โ€” and that one difference predicts most of the rest of the table.

The comparison matrix

The exhaustive side-by-side. Use the controls to filter rows down to one dimension or highlight a single tool's column. (Both persist across reloads; Copy as Markdown always exports the full matrix.)

Filter rows
Highlight column
View
Dimension Temporal Apache Airflow Step Functions
CategoryDurable-execution engineDAG-based batch schedulerManaged serverless state machine
Core philosophyWrite failure-proof codePipelines as code, run on timeOrchestrate AWS with a JSON state machine
Authoring modelImperative code (workflows + activities)Python DAGs (operators / sensors / TaskFlow)Declarative ASL JSON or Workflow Studio
Unit of workWorkflow + ActivityTask (an operator instance)State (Task / Choice / Map / Parallel / โ€ฆ)
Language supportGo, Java, Python, TypeScript, .NET, PHPPython (task bodies can shell out to anything)None โ€” JSON DSL; logic lives in Lambda/services
Durability mechanismEvent-sourced history + deterministic replayScheduler re-runs tasks; state in metadata DBManaged service persists each state transition
Execution guaranteeExactly-once effect via replay + idempotent activitiesAt-least-once task runs; relies on idempotencyStandard: exactly-once ยท Express: at-least-once
Max run durationEffectively unbounded (years); bounded by history sizeNo hard per-run cap; tasks bounded by timeoutsStandard: 1 year ยท Express: 5 minutes
Long waits / human-in-loopDurable timers, signals, updates (months, $0 idle)Sensors / deferrable operators; awkward for live inputWait state; .waitForTaskToken
TriggeringCode / API-driven (start a workflow); cron via SchedulesTime-driven (cron / timetables) + data-driven AssetsEvent / API-driven (StartExecution, EventBridge, API Gateway)
State & data passingPlain in-memory variables; activity return valuesXCom (small payloads via metadata DB)JSON in/out via JSONPath/JSONata; variables
Error handling / retriesPer-activity retry policies; sagas/compensation in codeTask retries / retry_delay; trigger rules; callbacksRetry / Catch in ASL; per-state backoff
Concurrency & scaleStateless workers; sharded history; namespacesScheduler + Celery/K8s workers; pools/parallelismFully managed autoscale; high per-account quotas
ArchitectureTemporal Service (Frontend/History/Matching) + your workers + DBScheduler + webserver + DAG processor + workers + triggerer + DBOpaque managed service; you see only the API/console
DeploymentSelf-host OSS or Temporal CloudSelf-host or managedManaged-only (AWS)
First-party managedTemporal CloudAWS MWAA, Google Cloud ComposerNative AWS service
Observability / UITemporal Web UI (per-execution event history)React UI: Grid / Graph / logsConsole graph + Workflow Studio; Express logs โ†’ CloudWatch
VersioningWorkflow versioning APIs / Worker VersioningDAG versioning (a run finishes on its start version)State-machine versions + aliases
Testing / local devTime-skipping test framework; local dev serverairflow dags test; unit-test operators; local DockerTestState API / Step Functions Local; mostly cloud
Pricing modelOSS + infra, or Temporal Cloud per-actionOSS + infra; MWAA/Composer per-environmentStandard $0.000025/transition ยท Express $1.00/M req + $0.00001667/GB-s
Vendor lock-inLow (OSS, portable, multi-cloud)Low (OSS, portable); managed variants add someHigh (AWS-only; ASL is AWS-specific)
EcosystemSDKs + your own activitiesHuge provider/operator catalog200+ AWS SDK integrations + optimized integrations
Best-fit use casesLong-running stateful logic, sagas, microservice orchestrationETL/ELT & ML batch pipelines, scheduled jobsServerless app orchestration, AWS-native ETL, event glue
Sharpest footgunDeterminism constraints on workflow codeIt is not durable execution; scheduler semanticsASL verbosity + deep AWS coupling

Execution & durability

"What survives a crash?" is the question that separates these three most sharply. They give three genuinely different answers, and confusing them is the most common mistake in this space.

  • Temporal replays a log. Every step a workflow takes is appended to a durable event history. When a worker dies mid-execution, a fresh worker re-runs the function from the top, but each already-recorded step returns its stored result instantly instead of re-executing. When replay catches up to the last event, the in-memory state โ€” locals, call stack, pending awaits โ€” is exactly as it was, and execution continues. The function literally resumes mid-line. (This is the subject of the companion page.)
  • Airflow re-runs tasks. There is no replayed call stack. The scheduler tracks each task's state (queued, running, success, failed) in the metadata database. If a task or worker dies, the scheduler simply re-queues that task from the start, up to its retries count. Durability is therefore an emergent property of idempotent tasks plus the metadata DB, at task granularity โ€” not a resumed program.
  • Step Functions persists transitions. The managed service records the result of every state transition. If a downstream service fails, the state machine retries or catches per its ASL rules; the execution itself is never "lost" because AWS holds its state. Standard workflows give exactly-once execution of each state; Express trades that for throughput and is at-least-once.
A worker / service dies mid-run โ€” what happens next? Temporal replays the log into a fresh worker history replay new worker re-runs recorded steps skipped resumes mid-function, no progress lost Airflow scheduler re-queues the failed task task โœ“ task ๐Ÿ’ฅ died retry re-run metadata DB task granularity โ€” tasks must be idempotent Step Functions service resumes from last saved transition AWS-managed โ€” state persisted for you state โœ“ state โœ“ resume here Standard: exactly-once Express: at-least-once
Three answers to "what survives a crash?": a replayed event log, a re-queued idempotent task, or a managed service holding the state machine's position.
The distinction that trips everyone up

Airflow is a scheduler, not a durable-execution engine. It does not replay a function or resume a call stack โ€” it re-runs tasks, and its durability is only as good as your tasks' idempotency plus what is recorded in the metadata DB. If you need a months-long process that resumes exactly where a function blocked, that is Temporal's model (or, within AWS, a Step Functions Standard workflow), not Airflow's.

Same workflow, three ways

One toy use case, written in each tool: fetch an order, total it, then branch โ€” notify a manager for big orders, otherwise send a receipt. The shape of each program is the philosophy made concrete.

Temporalโ€” Python, the logic is the program
NO_RETRY = RetryPolicy(maximum_attempts=1)   # Temporal retries โˆž by default; opt out

@workflow.defn
class OrderWorkflow:
    @workflow.run
    async def run(self, order_id: str) -> str:
        order = await workflow.execute_activity(
            fetch_order, order_id,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=RetryPolicy(maximum_attempts=5),   # opt in: retry the fetch
        )
        total = await workflow.execute_activity(
            compute_total, order, retry_policy=NO_RETRY)
        if total > 1000:                          # ordinary branching, in code
            await workflow.execute_activity(
                notify_manager, order_id, retry_policy=NO_RETRY)
        else:
            await workflow.execute_activity(
                send_receipt, order_id, retry_policy=NO_RETRY)
        return "done"
Apache Airflowโ€” Python that declares a DAG; the bodies do the work
@dag(schedule="@daily", start_date=datetime(2026, 1, 1), catchup=False)
def order_pipeline(order_id: str = "A-42"):     # run input, via DAG params
    @task(retries=5)                            # auto-retried
    def fetch_order(order_id: str) -> dict:
        return api.get_order(order_id)

    @task
    def compute_total(order: dict) -> float:
        return sum(i["price"] for i in order["items"])

    @task.branch                            # pick the next task by id
    def route(total: float) -> str:
        return "notify_manager" if total > 1000 else "send_receipt"

    @task
    def notify_manager(order_id: str): ...
    @task
    def send_receipt(order_id: str): ...

    total = compute_total(fetch_order(order_id))
    route(total) >> [notify_manager(order_id), send_receipt(order_id)]

order_pipeline()
Step Functionsโ€” ASL JSON; logic lives in the services it calls
{
  "StartAt": "FetchOrder",
  "States": {
    "FetchOrder": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": { "FunctionName": "fetchOrder",
                       "Payload": { "order_id.$": "$.order_id" } },
      "Retry": [ { "ErrorEquals": ["States.TaskFailed"],
                  "MaxAttempts": 5, "BackoffRate": 2.0 } ],
      "Next": "ComputeTotal"
    },
    "ComputeTotal": {
      "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
      "Next": "Route"
    },
    "Route": {
      "Type": "Choice",
      "Choices": [ { "Variable": "$.total",
                    "NumericGreaterThan": 1000, "Next": "NotifyManager" } ],
      "Default": "SendReceipt"
    },
    "NotifyManager": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "End": true },
    "SendReceipt":   { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", "End": true }
  }
}

Read top to bottom: Temporal's branch is a Python if; Airflow's is a @task.branch that returns the id of the next task in the graph; Step Functions' is a declarative Choice state. None of the real work happens in the Step Functions document โ€” it happens in the Lambda functions and SNS topics it points at.

Scheduling & triggering

How does a run begin? This is one of the most practical differences, and it reflects each tool's heritage.

  • Airflow is time-first. Its native instinct is the cadence: a DAG declares a schedule (a cron string or a timetable) and the scheduler creates a run per interval. Modern Airflow adds data-aware scheduling โ€” a DAG can run when its upstream Assets update โ€” and external event triggers, but the batch-interval model is its center of gravity.
  • Step Functions is event/API-first. An execution starts on an explicit StartExecution call โ€” directly, from API Gateway, or, most commonly, from EventBridge rules and schedules. It is built to react to events flowing through AWS, with cron available via an EventBridge schedule.
  • Temporal is code/API-first. A workflow starts when a client calls start_workflow() from anywhere in your system โ€” typically in response to a user action or an event. For recurring runs, Temporal Schedules provide cron-like and calendar triggering as a first-class server feature.

Rule of thumb: if you think in "every night at 2am, process yesterday's data," that is Airflow's home turf. If you think in "when this event happens, run this process," Step Functions and Temporal start a run per event rather than per interval.

State, data & retries

Passing data between steps

Because Temporal workflows are ordinary functions, intermediate state is just local variables and activity return values held in memory (and reconstructed on replay). Airflow tasks are separate processes, so values move through XCom, persisted in the metadata DB and meant for small payloads โ€” bulk data should travel through external storage (S3, a warehouse) with only references in XCom. Step Functions threads a JSON document through the states, reshaped per state with JSONPath or JSONata, with a hard 256 KB payload limit between states (again: keep large data in S3 and pass pointers).

Payloads are not a place to put your data

Both Airflow's XCom and Step Functions' state payload are deliberately small. The pattern in both is identical: keep large objects in object storage and pass handles. Temporal activity results can be larger, but the same discipline pays off โ€” the event history (and thus replay cost) grows with what you record.

Retries & failure

All three express retries declaratively, per step โ€” but their defaults are opposite, which is easy to miss. Temporal retries every activity automatically by default (unlimited attempts, exponential backoff), so you attach a retry policy to cap it โ€” or, as in the code above, a maximum_attempts=1 policy to switch it off; it also models all-or-nothing flows as sagas with explicit compensation in code. Airflow and Step Functions are the reverse โ€” a step does not retry unless you opt in: Airflow via retries/retry_delay (default 0) plus trigger rules and on_failure_callback hooks; Step Functions via Retry and Catch blocks per state, routing caught errors to a fallback. So matching them up takes extra code on the Temporal side (the NO_RETRY policy) and zero on the others โ€” the opt-out-vs-opt-in difference made literal.

Architecture

What you actually operate is wildly different across the three โ€” from "a database plus a few stateless services" to "literally nothing."

Airflow: several cooperating components

Airflow self-hosted is a small distributed system around a central metadata database. The scheduler decides what to run and hands tasks to an executor (Local, Celery, or Kubernetes); the DAG processor parses your Python DAG files; workers execute tasks; the triggerer runs deferred/async waits; and a webserver / API server serves the UI and REST API. Everyone coordinates through the metadata DB.

Metadata DB Postgres / MySQL the single source of coordination DAG files *.py DAG Processor parses + serializes Scheduler Executor (Local/Celery/K8s) Webserver UI + REST API Workers run the task code Triggerer async deferred waits
Airflow self-hosted: a handful of stateless components orbiting one metadata database. Managed Airflow (MWAA, Cloud Composer) provisions exactly this for you.

Step Functions: a black box

There is no architecture to run. Step Functions is a fully managed AWS service: you register a state machine and call StartExecution. AWS owns the scheduler, the durable store, the retries, and the scaling. Your "components" are the AWS services each Task state invokes โ€” Lambda, SQS, SNS, DynamoDB, ECS, and, via SDK integrations, 200+ services and thousands of API actions.

your app / EventBridge StartExecution Step Functions fully AWS-managed โ€” no infra for you Task โ†’ Choice Map โ†’ Parallel retries persisted 200+ AWS services Lambda SQS SNS DynamoDB ECS โ€ฆSDK request-response ยท run-a-job (.sync) ยท wait-for-callback
Step Functions has no operable architecture: AWS runs the state machine and you wire it to other AWS services through three integration patterns.

Temporal: a service plus your workers

Temporal sits in between: a Temporal Service (its Frontend, History, and Matching roles, backed by a database such as Cassandra, MySQL, or PostgreSQL) plus your own stateless workers that host the workflow and activity code and long-poll for tasks. You run the cluster (or buy Temporal Cloud) and you run the workers; the durable state lives in the service. The internals get a full treatment on the durable-execution page.

Deployment & managed offerings

This is where lock-in is decided. Two of the three are open source and portable; one is a single cloud.

 Self-hostFirst-party managedPortability
TemporalYes โ€” OSS server (MIT)Temporal CloudHigh โ€” same OSS anywhere; multi-cloud
AirflowYes โ€” OSS (Apache 2.0)AWS MWAA ยท Google Cloud Composer ยท (Astronomer, 3rd-party)High โ€” real Apache Airflow across vendors
Step FunctionsNoAWS only (the service itself)Low โ€” ASL and integrations are AWS-specific

For Airflow, note the distinction the marketing pages blur: MWAA and Cloud Composer are managed deployments of the real Apache Airflow โ€” not forks โ€” so a DAG can in principle move between them and a self-hosted cluster. Astronomer is the main commercial vendor (and steward of much of the project), but it is third-party rather than a hyperscaler offering. Step Functions has no self-hosted form at all: choosing it is choosing AWS.

Scale & limits

All three scale to serious production load, but they hit different ceilings.

  • Temporal scales by adding stateless workers and by sharding workflow histories across the cluster; high-scale users run millions of concurrent executions. The practical limit per workflow is event-history size โ€” long-running loops use Continue-As-New to start a fresh history and avoid unbounded growth.
  • Airflow scales workers horizontally (Celery or Kubernetes) and throttles with pools and parallelism settings. Its historical bottleneck is the scheduler and metadata DB under very high task volume โ€” a constraint teams operating Airflow at scale have written about candidly (Shopify's account of running thousands of DAGs is a good primary read). It is tuned for throughput of batch work, not low-latency per-event execution.
  • Step Functions autoscales transparently. The numbers are AWS account quotas (some soft/adjustable): Standard supports thousands of state transitions per second; Express is built for very high event rates (on the order of 100,000 executions/second). The hard caps that bite are the duration limits โ€” Standard 1 year, Express 5 minutes โ€” and the 256 KB payload between states.

Observability & versioning

Watching a run

Each ships a first-party UI tuned to its model. Airflow's React UI is pipeline-ops centric: the Grid and Graph views show task status across runs, with logs per task instance. Step Functions' console renders the state machine graph and animates each execution's path, with Standard executions keeping full history in-console and Express sending logs to CloudWatch. Temporal's Web UI is execution-centric: it shows the complete event history of any single workflow, which is the natural debugging unit for durable execution.

Changing a running workflow

Long-lived workflows must survive their own code changing underneath them. Temporal takes this most seriously because replay makes it hard: workflow versioning (patching) and Worker Versioning let new code coexist with in-flight executions (detailed on the durable-execution page). Airflow provides DAG versioning so a run completes on the DAG version it started with. Step Functions supports versions and aliases of a state machine, so you can publish an immutable version and shift traffic deliberately.

Cost models

Three different shapes of bill, which matter as much as features at scale.

  • Temporal โ€” the OSS server is free; you pay for the infrastructure you run it on (and the operational effort). Temporal Cloud instead bills per action (workflow and activity operations) plus storage, so cost tracks usage rather than fixed servers.
  • Airflow โ€” OSS is free plus infrastructure. Managed Airflow is priced per environment: MWAA and Cloud Composer charge for the running environment (size/uptime) regardless of how busy your DAGs are, so an idle Airflow still costs money.
  • Step Functions โ€” pure pay-per-use, and the two workflow types bill on different axes:
    • Standard: $0.000025 per state transition, with 4,000 free transitions/month. Predictable, audit-grade, can run a year.
    • Express: $1.00 per million requests plus $0.00001667 per GB-second of duration. Cheap at very high volume and short durations.
The cost intuition

Temporal and self-hosted Airflow are a fixed-ish infrastructure bill that amortizes well under heavy, steady load. Managed Airflow is a per-environment bill that suits always-on pipelines. Step Functions is per-execution, which is unbeatable for spiky, low-volume serverless work and can get expensive for very chatty, high-frequency Standard state machines (watch the transition count).

When to use which

The honest one-liners, then a flow.

  • Reach for Temporal when the work is long-lived, stateful application logic that must survive crashes and deploys โ€” sagas with compensation, multi-day human-in-the-loop, microservice orchestration โ€” expressed as ordinary code in your language and portable across clouds.
  • Reach for Airflow when the work is scheduled batch data pipelines (ETL/ELT/ML) and you want a rich operator ecosystem plus a pipeline-ops UI, and your team thinks in Python and dependency graphs. Not for per-request or durable-app logic.
  • Reach for Step Functions when you are all-in on AWS and want to orchestrate AWS services with minimal ops and pay-per-use. Choose Standard for long-running, auditable, exactly-once flows (up to a year); Express for high-volume, short (<5 min), at-least-once event processing.
Long-lived, stateful, resume mid-function? yes Temporal no Scheduled batch data pipelines? yes Airflow no All-in on AWS, serverless glue? yes Step Functions no reconsider the need sagas, human-in-loop, microservice orchestration ETL/ELT/ML on a cron cadence Standard = long/exactly-once Express = short/high-volume Tie-breaker: worried about lock-in or multi-cloud? Temporal or self-hosted Airflow, never Step Functions.
A rough triage. Real choices weigh team skills, existing stack, and operational appetite โ€” but the first question (does it need to resume mid-function?) does most of the work.

Common misconceptions

"Airflow gives you durable execution"

No. Airflow is a scheduler that re-runs tasks; it has no replay and no resumed call stack. Durability is your tasks being idempotent plus the metadata DB. For resume-mid-function semantics, use Temporal (or a Step Functions Standard workflow).

"Step Functions Standard and Express are the same but priced differently"

They have different guarantees: Standard is exactly-once and runs up to a year; Express is at-least-once and capped at five minutes. That difference, not just price, dictates which you can use.

"ASL is just another programming language"

ASL is a declarative JSON DSL that orchestrates; it has no general-purpose computation. The actual logic lives in the Lambda functions and AWS services each Task state invokes.

"Temporal can run forever, with no limits"

Effectively unbounded, not literally unlimited: a single execution is bounded by its event-history size, which is why long loops call Continue-As-New to reset history.

"MWAA / Cloud Composer are AWS's and Google's own forks of Airflow"

They are managed deployments of the real Apache Airflow, so DAGs stay portable. The chief commercial vendor, Astronomer, is a third party โ€” not a hyperscaler service.

Further reading

Temporal

Apache Airflow

AWS Step Functions