Temporal is a general-purpose durable execution platform with eight official SDKs (Go, Java, Python, TypeScript, Ruby, PHP, .NET, and Rust). Its lineage runs back a decade to Cadence at Uber, and it has the battle scars to show for it. If your problem is keeping a long-running process alive across crashes, retries, and hours-long waits, especially across more than one language, that’s what Temporal is for, and it’s good at it.
ZenML is the orchestrator for pipelines and agents in Python: training runs, batch inference, evaluation pipelines, tool-use loops, all written as steps. A dynamic pipeline builds its graph at run time from plain Python. wait() pauses a run for a human and picks it back up later. Steps retry, and a failed run retries without redoing the steps that finished. Every step output lands as a versioned artifact in your own store. Swap the stack and the same pipeline runs on Kubernetes, Vertex AI, SageMaker, AzureML and more.
Here’s the difference: Temporal keeps any execution alive. ZenML keeps an AI run reproducible: what ran, on which inputs, and what came out. You don’t need Temporal underneath ZenML to run an agent durably. You do need Temporal when the workflow isn’t an AI workload, or when it has to be written in Go.
ZenML
Use ZenML if you are
- Orchestrating Python pipelines and agents (training, batch inference, evaluation, tool-use loops) and want versioned artifacts and lineage for free, not as glue code
- Pausing an agent run for a human approval with wait(), then resuming it from the dashboard, CLI, or API
- Iterating on a pipeline where the expensive part is re-running the steps you didn't change
- Running in your own cloud (Kubernetes, Vertex AI, SageMaker, AzureML and more) and want one pipeline definition that works on all of them
Temporal
Use Temporal if you are
- Running a polyglot fleet where Go, Java, and TypeScript services need one durability contract
- Running general workflows (billing, provisioning, ETL, saga patterns), not AI workloads
- Depending on mid-step journal resume, per-signal event handling, or timers that fire inside a single long function
- Leaning on task queues, rate limits, namespacing, and a service tier with years of production behind it
Temporal keeps any execution alive. ZenML keeps an AI run alive and tells you afterwards exactly what it produced. No second engine underneath.
A simpler ops model for AI workloads
Temporal is the Temporal Service plus Workers, self-hosted or on Temporal Cloud. ZenML is a ZenML server plus a stack: an orchestrator, an artifact store, and whatever else the job needs, say a sandbox or a deployer. Each gives you a control plane and somewhere for work to run. What they keep track of is different.
- Service tier: Temporal Server is four services (Frontend, History, Matching, Worker) on top of a persistence database. The ZenML server stores pipeline definitions, run metadata, and artifact references; the artifacts themselves sit in the stack’s artifact store on S3, GCS, Azure Blob, or MinIO.
- Determinism: Temporal Workflow code has to be deterministic; anything that isn’t, like an external call, goes in an Activity. ZenML doesn’t care what a step body does. It’s an ordinary Python function, and whatever it returns gets stored as an artifact.
- Portability: Temporal Workers run wherever you deploy them. ZenML’s lever is the stack:
zenml stack setrepoints the same pipeline from a local orchestrator to Kubernetes, Vertex AI, SageMaker, AzureML, Airflow, Databricks and more, no code change. Self-host the server or use ZenML Pro; the artifacts stay in your bucket either way.
Pause for approval, resume later
Temporal’s answer to a human in the loop is wait_condition plus a signal: the Worker holds the workflow, and event history replays it after a restart. ZenML’s answer is wait() inside a dynamic pipeline. The run pauses, someone answers the question, and the run picks up where it stopped.
await workflow.wait_condition(lambda: self.approved is not None)Worker holds the workflow; history replays on recovery- status
- PAUSED · orchestration process torn down
- resolve
- dashboard · CLI · API → true
- resume
- zenml pipeline runs resume 9f2a
- Dynamic pipelines:
@pipeline(dynamic=True)builds the graph at run time from plain Python. Loop over agent tasks, branch on a result, fan out with.map().wait()lives here and only here: not inside a step body, not in a static pipeline. - What happens while it waits:
wait(schema=bool, question=...)polls until its timeout, then marks the runPAUSEDso the orchestration process can go away. Answer it from the dashboard, the API, orzenml pipeline runs wait-conditions resolve, thenzenml pipeline runs resumecarries on. Finished steps aren’t re-run.on_pauseandon_resumehooks fire on the way. - The gap: Temporal can wait anywhere inside a function and juggle many signals per workflow. ZenML waits between steps, one typed condition at a time.
Caching that skips the expensive steps
In an AI pipeline the expensive unit is usually one step: a training run, a batch of embeddings, a sweep of LLM calls. Change the last step and re-run, and you shouldn’t pay for the first four again. Temporal’s answer is Workflow Event History, which replays completed Activity results when a workflow resumes after a failure. ZenML’s answer is step caching, and it kicks in on every run, not just recovery.
replay completed Activities on resumerecovery path only · a clean re-run starts from scratch- step
- research(topic="AI orchestration")
- cache key
- step + parameters + inputs → unchanged
- Cache key: ZenML keys the cache on the step, its parameters, and its inputs. Nothing changed? The stored artifact comes back and the step body never runs. Switch it per step or per pipeline with
@step(enable_cache=True)and@pipeline(enable_cache=False). - Failure handling:
StepRetryConfig(max_retries=3, delay=10, backoff=2)retries a step. Execution modes decide whether the rest of the run fails fast, stops, or keeps going.zenml pipeline runs retryrestarts a failed dynamic run, and completed steps are reused rather than re-executed. ZenML retries the step; Temporal resumes mid-function from the journal. - Sandboxes: An agent that runs generated code gets a
sandboxstack component with local, Docker, Kubernetes, and Modal flavors:Client().active_stack.sandbox.create_session(), thensession.exec([...]). Docker and Modal add snapshot and restore. Temporal leaves isolation up to your Activity.
Artifact lineage across runs, not just event history
Temporal’s Workflow Event History is a replay log, not an artifact store. Want to know what last Tuesday’s workflow produced? You thread your own artifact references through Activity return values. ZenML stores every step output as a versioned artifact and pins it to the run.
- Artifacts: Every step return value is stored, versioned, and browsable on the run. Name one with
Annotated[dict, "training_data"]and you fetch it by name instead of hunting for a run id. - Cross-run load: A later run, or a notebook, pulls an earlier run’s artifact by name with
get_artifact_version(...).load()on the ZenML client. Lineage across runs is the default, not something you bolt on. - Deployments:
zenml pipeline deploy my_module.my_pipeline --name svcturns the pipeline into a long-running HTTP service, and every request becomes a tracked run with the same artifacts and lineage. Want to test a change to that agent against real production runs before it ships? Pair ZenML with Kitaru, replay-based regression testing for agents.
What makes ZenML different
| Feature | ZenML | Temporal | What that means |
|---|---|---|---|
| Durable human-in-the-loop wait and resume | Yes | Yes | Temporal: wait_condition plus signals. ZenML: wait() in a dynamic pipeline; the run pauses and resumes with completed steps reused. |
| Loops, branches, and fan-out decided at run time | Yes | Yes | ZenML dynamic pipelines use plain Python control flow; Temporal workflows do the same under determinism rules. |
| Recover after failure without re-executing completed work | Partial support | Yes | Temporal resumes mid-function from event history. ZenML retries at step granularity and reuses completed steps. |
| Caching on ordinary re-runs, not only on recovery | Yes | Not supported | ZenML skips steps whose parameters and inputs are unchanged on every run. |
| Versioned artifacts and lineage across runs | Yes | Not supported | Step outputs are stored and addressable; Temporal leaves artifact handling to you. |
| Isolated sandboxes for agent tool loops | Yes | Not supported | ZenML sandbox stack component (local, Docker, Kubernetes, Modal). Temporal leaves isolation to the Activity. |
| Serve as an HTTP service with a tracked run per request | Yes | Partial support | zenml pipeline deploy. Temporal workflows are started from your own service. |
| One stack abstraction for your clouds | Yes | Not supported | Configure once, every pipeline uses it. Temporal Workers are deployed per environment. |
| Polyglot SDKs (Go, Java, TypeScript, Ruby, PHP, .NET, Rust) | Not supported | Yes | ZenML is Python only. |
| Task queues, rate limits, and concurrency keys | Not supported | Yes | ZenML leaves queueing to the orchestrator. |
| Native cron scheduling and namespacing | Partial support | Yes | ZenML hands scheduling to the orchestrator; works on Kubernetes, Vertex AI, SageMaker, Airflow and others, not on local. |
| Open source, self-hostable | Yes | Yes |
How the two surfaces map
| Concept | Temporal | ZenML |
|---|---|---|
| Workflow boundary | @workflow.defn | @pipeline or @pipeline(dynamic=True) |
| Unit of work | Activity (non-deterministic) | @step (ordinary Python) |
| Determinism requirement | Workflow must be deterministic | No determinism requirement on step bodies |
| Skipping completed work | Event history replay on recovery | enable_cache on every run; completed steps reused on retry and resume |
| Pause / resume | wait_condition + signal | wait(schema, question) in a dynamic pipeline; zenml pipeline runs resume |
| Where it runs | Workers you deploy per environment | Stack orchestrator (zenml stack set) |
| Outputs | Thread through Activity returns | Versioned artifacts on the run, loadable by name |
| Retries | Activity retry policy | StepRetryConfig(max_retries=..., delay=..., backoff=...) and zenml pipeline runs retry for a failed dynamic run |
| Serving | Start workflows from your own service | zenml pipeline deploy, one tracked run per request |
Code comparison
from typing import Annotated
from zenml import pipeline, step, wait
from zenml.config.retry_config import StepRetryConfig
@step(retry=StepRetryConfig(max_retries=3, delay=10, backoff=2))
def plan(goal: str) -> Annotated[list[str], "tasks"]:
return call_llm(f"Break down: {goal}")
@step
def research(task: str) -> Annotated[str, "finding"]:
return call_llm(f"Research: {task}")
@step
def deploy(findings: list[str]) -> Annotated[dict, "release"]:
return ship(findings)
@pipeline(dynamic=True)
def agent_pipeline(goal: str):
tasks = plan(goal)
findings = research.map(task=tasks) # fan-out decided at run time
# Pause for approval. The run goes PAUSED, the process can be
# torn down, and a human resolves it from the dashboard or CLI.
approved = wait(schema=bool, question="Approve and deploy?")
if approved:
deploy(findings)
agent_pipeline("prepare the release notes")
# Resolve and resume later; completed steps are reused:
# zenml pipeline runs wait-conditions resolve --run <id> --interactive
# zenml pipeline runs resume <id>
# Same code on another cloud, no edits:
# zenml stack set vertex_stack && python agent_pipeline.pyfrom datetime import timedelta
from temporalio import activity, workflow
@activity.defn
async def plan(goal: str) -> list[str]:
return await call_llm(f"Break down: {goal}")
@activity.defn
async def research(task: str) -> str:
return await call_llm(f"Research: {task}")
@workflow.defn
class AgentFlow:
def __init__(self) -> None:
self._approved: bool | None = None
@workflow.signal
def approve(self, ok: bool) -> None:
self._approved = ok
@workflow.run
async def run(self, goal: str) -> str:
tasks = await workflow.execute_activity(
plan, goal, start_to_close_timeout=timedelta(minutes=5)
)
findings = [
await workflow.execute_activity(
research, t, start_to_close_timeout=timedelta(minutes=5)
)
for t in tasks
]
# Durable wait: the Worker holds the workflow, history
# replays it after a restart.
await workflow.wait_condition(lambda: self._approved is not None)
return "\n".join(findings) if self._approved else "Rejected"
# Run via: await client.execute_workflow(AgentFlow.run, goal,
# id=..., task_queue=...); approval arrives via
# client.get_workflow_handle(...).signal(AgentFlow.approve, True).One orchestrator for
pipelines and agents
If your durability problem spans Go services, Java backends, cron-scheduled ETL, or sagas with dozens of signals per workflow, Temporal is the tool. If the work is a Python pipeline or agent that needs an approval gate, versioned outputs, step retries, and the same code on Kubernetes today and Vertex AI next quarter, ZenML is the orchestrator. You don’t need Temporal underneath it.









