Compare

AI orchestration or durable execution for services

Restate makes services durable: a journal, Virtual Objects, durable RPC, several languages. ZenML is open-source AI orchestration for Python: dynamic pipelines, wait() approvals, retries, sandboxes, versioned artifacts, one stack for your clouds.

Restate is a durable execution engine for services. Handlers register with the Restate server, every action gets journalled, and a crashed handler resumes from the journal instead of from the top. Virtual Objects give you keyed, single-writer state; durable RPC and promises let services call each other without losing work. It does all that across TypeScript, Java, Go, Kotlin, Rust, and Python, and it does it well.

ZenML is the orchestrator for pipelines and agents in Python. 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. Tool loops get an isolated sandbox, and every step output lands as a versioned artifact in your own bucket.

Restate makes a service resilient. ZenML makes an AI run something you can pause, re-run, and move between clouds. If the workload is an agent or a pipeline, ZenML is the whole answer; you don’t need Restate underneath it. If the workload is a fleet of services with keyed state and RPC between them, that’s Restate’s job.

ZenML

Use ZenML if you are

  • Orchestrating Python pipelines and agents (training, batch inference, evaluation, tool-use loops) and want versioned artifacts and lineage by default
  • Pausing an agent run for a human approval with wait(), then resuming it from the dashboard, CLI, or API
  • Targeting Kubernetes, Vertex AI, SageMaker, AzureML and more, and want one pipeline definition that runs on all of them
  • Loading an earlier run's output back out, by name, months later

Restate

Use Restate if you are

  • Making distributed services resilient, where journalled recovery and durable RPC are the hard part
  • Wanting keyed, single-writer state from Virtual Objects instead of reaching for a database
  • Operating across TypeScript, Java, Go, Kotlin, or Rust as well as Python
  • Depending on resume from inside a single handler, or on many awakeables and promises per invocation
Restate keeps your services alive. ZenML keeps your pipelines and agents alive, and remembers what they produced. Pick by the shape of the workload.

Pipeline-shaped vs service-shaped

Restate’s unit is the handler: a service endpoint where every action gets journalled so it can resume. ZenML’s unit is the step: a Python function whose output is stored, versioned, and wired to the steps around it. A dynamic pipeline decides at run time which steps exist at all.

Restate · app-shapedHandlers for services, state, and workflows
serviceRPC-shaped durable handlers
virtual objectkeyed per-entity state, strongly consistent
workflowlong-running, journaled, resumable
Durability primitives, built for app workloads.
ZenML · pipeline-shapedDynamic pipelines, wait(), versioned artifacts, one stack
@pipelinewraps your agent
@stepordinary Python · output stored as a versioned artifact
wait()pause a dynamic pipeline for approval, resume later
enable_cacheunchanged steps skipped on every re-run
zenml stack setKubernetes, Vertex AI, SageMaker, AzureML, no code change
Your agent code runs inside the step, unchanged.
  • @step: Ordinary Python, no determinism rules on the body. Its return value lands as a versioned artifact, not a journal entry. StepRetryConfig(max_retries=3, delay=10, backoff=2) retries it; zenml pipeline runs retry restarts a failed dynamic run, and completed steps are reused rather than re-executed.
  • @pipeline(dynamic=True): Loop over agent tasks, branch on a result, fan out with .map(). The graph builds itself as the Python runs, and wait() can pause it between steps.
  • zenml stack set: The same pipeline runs on Kubernetes, Vertex AI, SageMaker, AzureML, Airflow, Databricks and more, no code change. Your agent or model code sits inside the step and never knows the difference.

Pause for approval, resume later

Restate’s answer to a human in the loop is a durable promise on a Workflow. The run handler awaits ctx.promise("approval"), and a second handler on the same Workflow resolves it later. ZenML’s answer is wait() inside a dynamic pipeline. The run pauses, someone answers the question, and the run picks up where it stopped.

Restate · durable promise
await ctx.promise("approval").value()Journalled by the server; a handler resolves it later
ZenML · @pipeline(dynamic=True)run 9f2a…
approved = wait(schema=bool, question="Approve and deploy?")
status
PAUSED · orchestration process torn down
resolve
dashboard · CLI · API → true
resume
zenml pipeline runs resume 9f2a
planreused
research ×3reused
deployruns now
Completed steps are reused on resume. The pipeline continues from the wait, and the whole run stays one tracked record.
  • What happens while it waits: wait(schema=bool, question=...) polls until its timeout, then marks the run PAUSED so the orchestration process can go away. Answer it from the dashboard, the API, or zenml pipeline runs wait-conditions resolve, then zenml pipeline runs resume carries on without re-running the steps that finished. on_pause and on_resume hooks fire on the way.
  • Sandboxes: An agent that runs generated code gets a sandbox stack component with local, Docker, Kubernetes, and Modal flavors: Client().active_stack.sandbox.create_session(), then session.exec([...]). Docker and Modal add snapshot and restore. Restate leaves isolation to the handler.
  • The gap: Restate resumes from any point inside a handler and can juggle many promises and awakeables per invocation. ZenML waits between steps, one typed condition at a time.

Artifact lineage vs journal and Virtual Object state

Restate’s journal is a replay log: it exists so a handler can resume correctly. Virtual Object state is a keyed store you read and write. Neither is an artifact history. Ask either one what a particular run produced three weeks ago and you get nothing back.

exec_id 9f2atoday · 14:02
@step research1,247 tok · 2.8s
brief.jsonv3
@step draft3,980 tok · 6.1s
draft.mdv3
diffacross runs
exec_id 7c1bMon · 11:47
@step research1,112 tok · 2.4s
brief.jsonv1
@step draft4,215 tok · 6.8s
draft.mdv1
Restate: handler progress is journaled; Virtual Object state is keyed per entity. Not surfaced as cross-run versioned artifacts with a diff view.
  • Artifacts: Every step return value goes to your artifact store, versioned and browsable on the run. Name it with Annotated[dict, "training_data"] and you fetch it by name. Steps you didn’t touch come from cache on every run, keyed on the step, its parameters, and its inputs.
  • Cross-run load: get_artifact_version(...).load() on the ZenML client pulls any earlier run’s output into a notebook or a later pipeline.
  • Comparison: Put two runs of the same pipeline side by side and compare what they produced, which is the question you actually ask after a prompt or model change. To test that change against real production runs before it ships, pair ZenML with Kitaru, replay-based regression testing for agents.

Server-registered handlers vs a library you import

Restate puts a server in the request path. ZenML puts a server beside it.

Restate · server-registeredRequest path flows through the server
caller
restate-serverRust · BSL 1.1
journalroutingreplay
handlerhandler
Handlers register with the server. Every invocation crosses the server boundary.
ZenML · embeddedFlow runs in your process; server holds metadata
your python process
@pipeline
@step
@step
uv add zenml · no server process in the request path
zenml servermetadata · UI · auth
Server is for the metadata, not the request path.
  • Restate: The Restate server is the coordinator. Handlers register with it and invocations flow through it; that’s what makes journalled recovery and durable RPC possible. Where it runs is up to you: Kubernetes, a VM, or serverless.
  • ZenML: pip install zenml, write a pipeline, run it. The ZenML server records runs, artifacts, and metadata. It doesn’t execute steps, and it isn’t in the request path of a deployed pipeline. Self-host it or use ZenML Pro; artifacts stay in your own bucket either way.
  • Serving: When a pipeline does need an endpoint, zenml pipeline deploy my_module.my_pipeline --name svc gives it one on local, Docker, Kubernetes, GCP Cloud Run, AWS App Runner, or Hugging Face, and every request becomes a run with full lineage.

What makes ZenML different

FeatureZenMLRestateWhat that means
Durable human-in-the-loop wait and resumeYesYesRestate: durable promises and awakeables. ZenML: wait() in a dynamic pipeline; the run pauses and resumes with completed steps reused.
Loops, branches, and fan-out decided at run timeYesYesZenML dynamic pipelines use plain Python control flow, as Restate handlers do.
Recover mid-handler without re-executing completed workPartial supportYesRestate resumes from its journal. ZenML retries at step granularity and reuses completed steps.
Caching on ordinary re-runs, not only on recoveryYesNot supportedZenML skips steps whose parameters and inputs are unchanged, on every run.
Versioned artifacts and lineage across runsYesNot supportedStep outputs are stored and loadable by name; Restate journals actions, not artifacts.
Isolated sandboxes for agent tool loopsYesNot supportedZenML sandbox stack component (local, Docker, Kubernetes, Modal). Restate leaves isolation to the handler.
One stack abstraction for your cloudsYesNot supportedConfigure once, every pipeline uses it. Restate leaves deployment to you.
Serve as an HTTP service with a tracked run per requestYesPartial supportzenml pipeline deploy. Restate handlers are HTTP-invocable but keep no run record of outputs.
Keyed, single-writer state (Virtual Objects)Not supportedYesNo ZenML equivalent; use a database.
Durable RPC and event subscriptions between servicesNot supportedYesZenML composes steps in a pipeline, not services over the network.
TypeScript, Java, Go, Kotlin, Rust as well as PythonNot supportedYesZenML is Python only.
Open source, self-hostableYesPartial supportZenML is Apache 2.0. The Restate server is BSL 1.1, converting to Apache 2.0 after four years; its SDKs are MIT. Self-hosting is supported.

How the two surfaces map

ConceptRestateZenML
BoundaryService, Virtual Object, or Workflow handler@pipeline or @pipeline(dynamic=True)
Unit of workJournalled action / ctx.run_typed@step (ordinary Python)
What persistsThe invocation journalStep completion plus outputs as versioned artifacts
Skipping completed workJournal replay on recoveryenable_cache on every run; completed steps reused on retry and resume
Durable pauseDurable promises (Workflow), awakeableswait(schema, question) in a dynamic pipeline; zenml pipeline runs resume
Keyed stateVirtual ObjectsBring your own store
Where it runsHandlers you deploy, coordinated by the Restate serverStack orchestrator (zenml stack set)
Cross-run reuseVirtual Object stateget_artifact_version(...).load()

Code comparison

ZenML
from typing import Annotated

from zenml import pipeline, step, wait

@step
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>
# No server in the execution path. Artifacts land in your bucket.
# Same code on another cloud:
#   zenml stack set vertex_stack && python agent_pipeline.py
Restate (Python SDK)
import restate
from restate import Workflow, WorkflowContext, WorkflowSharedContext

# Durable promises live on Workflow handlers, keyed by workflow id.
agent = Workflow("agent")

@agent.main()
async def run(ctx: WorkflowContext, goal: str) -> dict:
  # ctx.run_typed journals each result, so a crash resumes here
  # rather than re-executing the call.
  tasks = await ctx.run_typed("plan", call_llm, prompt=f"Break down: {goal}")
  findings = [
      await ctx.run_typed(f"research-{i}", call_llm, prompt=f"Research: {t}")
      for i, t in enumerate(tasks)
  ]

  # Durable promise: journalled by the server, resolved by the
  # approve handler below, survives restarts.
  approved = await ctx.promise("approval", type_hint=bool).value()
  if not approved:
      return {"status": "rejected"}
  return await ctx.run_typed("deploy", ship, findings=findings)

@agent.handler()
async def approve(ctx: WorkflowSharedContext, ok: bool) -> None:
  await ctx.promise("approval", type_hint=bool).resolve(ok)

# Handlers register with the Restate server, which coordinates
# invocations and owns the journal.
app = restate.app([agent])

One orchestrator for
pipelines and agents

If the problem is distributed services that have to survive crashes, talk over durable RPC, and hold keyed state, Restate is built for exactly that. If the problem 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 Restate underneath it.