Compare

AI orchestration vs event-driven durable functions

Inngest runs event-driven durable functions. ZenML is open-source AI orchestration for Python: dynamic pipelines, wait() approvals, caching, sandboxes, versioned artifacts in your own bucket, and one stack for your clouds.

Inngest is an event-driven durable functions platform. You send events, functions react, and each step.run is memoized so a retry doesn’t repeat finished work. You get sleeps that release compute, waits on future events, throttling and concurrency controls, and SDKs for TypeScript, Python and Go. For event-driven app workflows, it’s a well-built platform.

ZenML is AI orchestration for Python. It runs pipelines and agents on your own infrastructure and remembers what they did. A dynamic pipeline builds its graph from plain Python, and wait() pauses a run until a human or another system answers. Failed steps retry with backoff, steps you didn’t touch come from cache across runs, and agent tool loops run inside a sandbox.

Both skip finished work and both can stop a run and wait. Here’s the difference: who drives, and what survives. Inngest calls your function step by step and keeps the run history on its side. ZenML runs your pipeline where you already have compute, and what it keeps is a set of artifacts you can open a month later. You don’t need Inngest underneath ZenML to run an agent durably.

ZenML

Use ZenML if you are

  • Orchestrating Python pipelines and agents (training, batch inference, evaluation pipelines, agent workflows) and want versioned artifacts and lineage by default
  • Pausing a run for human approval, looping or fanning out at run time, and retrying failed steps without bolting on a second engine
  • Running agent tool loops in an isolated sandbox and serving a pipeline as an HTTP endpoint with a tracked run per request
  • Keeping artifacts and run history in your own infrastructure, not a vendor's coordination layer

Inngest

Use Inngest if you are

  • Building event-driven application workflows where event subscriptions, fan-out and throttling are the hard part
  • Wanting durable sleeps that release compute the moment they start and resume on a timer, for hours or days
  • Operating across TypeScript and Go as well as Python, under one contract
  • Happy to let a managed platform drive execution, with no orchestrator of your own to run
Inngest memoizes a step so the run can carry on. ZenML stores it so you can come back in a month and ask what the run produced, and why.

Pipelines you own vs functions the platform drives

Inngest flips control: you register functions, and the Inngest platform decides when to call each step and tracks progress on its side. ZenML keeps the pipeline as a program you run, with a stack orchestrator executing it and a server writing down what happened.

One orchestrator on your infrastructure versus a platform across your stack.Compares Inngest as a packaged event-driven platform with ZenML as the orchestrator that runs Python pipelines and agents on your own stack and stores artifacts in your bucket.
Inngest · platform across the stackRuntime + event bus + dashboard, packaged together
Inngest platform
events · cron · webhooksdurable engine · retries · stepsflow control · throttle · concurrencydashboard · replay · branch envs
your app re-shaped around it
Adopt the platform, get durability and the rest as one piece.
ZenML · one orchestrator, your infrastructureRuns pipelines and agents where you already have compute; no engine to operate
AgentPydantic AI, OpenAI Agents, LangGraph, raw Python
ZenML@pipeline(dynamic=True), @step, wait(), sandbox, deploy
StackKubernetes, Vertex AI, SageMaker, AzureML, Airflow, Databricks
Bucketversioned artifacts in your S3, GCS or Azure Blob
No second engine underneath. Your clouds run it; ZenML records it.
  • Who drives: Inngest calls back into your app step by step. ZenML hands the pipeline to the orchestrator in your stack (Kubernetes, Vertex AI, SageMaker, AzureML, Airflow, Databricks) and records the run.
  • Where the record lives: Inngest’s run history lives on the Inngest platform. ZenML’s lives in a server you can self-host, with the artifacts in your own bucket.
  • What you adopt: Inngest is a platform your app routes work through. ZenML is a library you write pipelines in. There’s no engine of its own to operate.

Durable waits and dynamic graphs vs polyglot durable functions

Inngest’s primitives are built for applications and span three languages. ZenML’s are built for pipelines and agents in Python, and they cover the agent cases people assume need a durable-functions engine.

Durable waits and dynamic graphs in Python versus polyglot durable functions.Compares Inngest step primitives across several languages with ZenML dynamic pipelines, wait(), caching and step retries for Python pipelines and agents.
Inngest · polyglot durable functionsSteps register with the engine across TypeScript, Python, Go
TypeScriptPythonGoKotlin/Java
step.rundurable step, return value persisted
step.sleeptimer that releases compute
step.wait_for_eventresume on a matching event send
step.invokecall another function across apps
Polyglot, app-shaped. Agent telemetry is glue you write on top.
ZenML · durable waits and dynamic graphs in PythonThe agent cases that usually get a second engine, inside the pipeline
@pipeline(dynamic=True)graph built from ordinary Python
wait()pause for a human or an external system; run marked PAUSED, resumed later
step.map()fan out over a list at run time; loops and branches are plain Python
StepRetryConfigper-step retries with delay and backoff
enable_cacheunchanged steps served from cache across runs, output versioned in your bucket
Python only. Approvals, retries and caching without a second engine.
  • @pipeline(dynamic=True): The graph builds at run time from plain Python: for loops, if branches, and step.map() fan-out over a list. No determinism rules on step bodies, and every branch taken leaves a versioned artifact.
  • wait(): Inside a dynamic pipeline, wait(schema=bool, question=...) pauses the run until someone answers from the dashboard, the CLI or the API. Until the timeout the orchestration process keeps polling. After that the run is marked PAUSED, the process can go away, and zenml pipeline runs resume picks it back up. Inngest’s step.wait_for_event and step.sleep release compute the moment they start.
  • StepRetryConfig and run retry: Per-step retries with delay and backoff, and zenml pipeline runs retry for a failed dynamic run, where completed steps are reused rather than re-executed. ZenML retries the step; Inngest resumes a function mid-body from memoized steps.
  • Sandboxes: A sandbox stack component gives an agent step an isolated session to run generated code in. It comes in local, Docker, Kubernetes and Modal flavors; Docker and Modal add snapshot and restore. Inngest has nothing like it. Fair’s fair: throttling, concurrency keys and event subscriptions have no ZenML equivalent either.

Where your data actually sits

In an AI workload the intermediate values are the sensitive part: prompts, retrieved documents, customer records, model outputs. Where those land is a procurement question as much as a technical one.

Customer-controlled data path versus managed cloud.Compares Inngest Cloud as a hosted engine and dashboard with ZenML self-hosting where artifacts and compute stay in the customer cloud.
Inngest · managed cloud is the easy pathEngine, queue, state, dashboard, hosted for you
your app
Inngest CloudSOC 2 · SAML · HIPAA BAA
durable engineflow controlreplay UI
Hosted and operated by Inngest
Alt: single-binary self-host
Spend buys managed convenience and a flow-control suite.
ZenML · self-hosted, one stack abstractionPick a stack; data and compute stay in your cloud
your @pipeline
ZenML servermetadata · step state · lineage · short-lived creds
your S3your GCSAzure Blob
KubernetesVertex AISageMakerAzureML
Spend buys internal platform ownership; data stays in your bucket.
  • ZenML: Artifacts go to your artifact store, meaning your S3, GCS or Azure Blob bucket. Self-host the server with the Helm chart, or use ZenML Pro as a managed control plane; the artifacts stay in your bucket either way.
  • Inngest: Step outputs pass through the Inngest platform; that’s how the run gets coordinated. Inngest does offer self-hosting. The managed cloud is the default path.
  • Why it matters: In a regulated shop, “our bucket” versus “a vendor’s coordination layer” tends to decide the security review.

Versioned artifacts vs event-sourced steps

Both keep a record of a run. Inngest’s is an event stream of what the platform did. ZenML’s is a set of artifacts you can open, including from the runs that stopped to ask a human.

Versioned artifacts and deployments versus event-sourced steps.Compares Inngest step memoization and replay tooling with ZenML typed artifacts, a paused wait condition, and a deployed pipeline where every request is a tracked run.
Inngest · step memoization + replay UIEngine persists step return values; dashboard turns it operational
function run
step.run · research memoizedstep.run · draft memoizedstep.wait_for_event · approve
replay buttonbulk canceltraces · alertsbranch envs
Operational tools follow from durable step memoization.
ZenML · artifacts + deploymentsTyped artifacts in your bucket, a paused run you can resume
run · review_pipeline
@step researchartifact: brief (v4, in your S3)
@step draftartifact: draft (typed, versioned)
wait() · human_approvalrun PAUSED; resolve, then zenml pipeline runs resume
zenml pipeline deploy run.review_pipeline --name review-svc
request= tracked runsame artifacts, same lineage
One record for a batch run and a served request.
  • Versions: Re-run a step with different inputs and you get a new version of the same named artifact, not an overwrite. Both stay loadable, and log_metadata() pins token counts, cost or model identity next to them.
  • Cross-run load: Client().get_artifact_version(...).load() pulls an earlier run’s output into a notebook or a later pipeline, by name.
  • Serving: zenml pipeline deploy stands a pipeline up behind an endpoint on local, Docker, Kubernetes, Cloud Run or App Runner, and every request becomes a run with the same artifacts and lineage a batch job gets. When the agent inside changes, Kitaru replays recorded production runs against the new version, which is a different job from orchestrating it.

What makes ZenML different

FeatureZenMLInngestWhat that means
Caching on ordinary re-runs, not only on retryYesPartial supportInngest memoizes steps within a run. ZenML caches across runs, keyed on the step, its parameters and its inputs.
Versioned artifacts and lineage across runsYesNot supportedStep outputs are stored and loadable by name; Inngest keeps step results in run history.
Graph built at run time from Python loops, branches and fan-outYesYesZenML: @pipeline(dynamic=True) with step.map(). Inngest: ordinary control flow around step.run, with parallel steps and events for fan-out.
Pause a run for human approval and resume it laterYesYesZenML: wait() in a dynamic pipeline, resolved from dashboard, CLI or API. Inngest: step.wait_for_event.
Durable sleeps that release compute immediatelyPartial supportYesZenML's wait() polls until its timeout, then marks the run PAUSED so the process can go away. Resume is a CLI command, or automatic in ZenML Pro.
Sandboxed code execution for agent tool loopsYesNot supportedStack component with local, Docker, Kubernetes and Modal flavors; Docker and Modal sessions can be snapshotted and restored.
Serve a pipeline as an HTTP endpoint with a tracked run per requestYesPartial supportzenml pipeline deploy. Inngest functions are invoked by events; the request/response service is your app.
One stack for Kubernetes, Vertex AI, SageMaker, AzureML, Airflow, DatabricksYesNot supportedConfigure once, every pipeline uses it.
Artifacts and run data stay in your own infrastructureYesPartial supportZenML writes to your bucket. Inngest self-hosting exists; the managed cloud is the default.
Resume a function mid-body from memoized stepsNot supportedYesZenML retries at step granularity; on a retried dynamic run, completed steps are reused rather than re-executed.
Throttling, concurrency keys and flow controlNot supportedYesZenML leaves this to the stack orchestrator.
Event subscriptions as triggersNot supportedYesZenML pipelines are invoked, scheduled through the orchestrator or ZenML Pro, or deployed; they don't subscribe to external events, though ZenML Pro can trigger a pipeline when another run finishes.
TypeScript and Go as well as PythonNot supportedYesZenML is Python only.
Open source, self-hostableYesPartial supportZenML is Apache 2.0. The Inngest server is SSPL with delayed Apache 2.0 publication, and its SDKs are Apache 2.0. Self-hosting is supported.

How the two surfaces map

ConceptInngestZenML
Workflow boundary@client.create_function(...)@pipeline or @pipeline(dynamic=True)
Unit of workstep.run (memoized)@step (cached, versioned output)
TriggerAn event you sendAn invocation, an orchestrator schedule, or a deployment
Run-time branching and fan-outControl flow around step.run, events for fan-outPython control flow and step.map() in a dynamic pipeline
Skipping completed workStep memoization within a runenable_cache across runs, plus zenml pipeline runs retry for a failed dynamic run
Durable pausestep.sleep, step.wait_for_eventwait(schema=..., question=...), run marked PAUSED after the timeout
Isolated tool executionIn your appClient().active_stack.sandbox.create_session()
Where it runsYour app, driven by the platformStack orchestrator (zenml stack set)
OutputsStep results in run historyVersioned artifacts in your bucket
Cross-run reuseBring your own storeClient().get_artifact_version(...).load()

Code comparison

ZenML
from typing import Annotated

from zenml import pipeline, step, wait
from zenml.client import Client

@step
def research(topic: str) -> Annotated[str, "brief"]:
  return call_llm(f"Research: {topic}")

@step
def draft(brief: str) -> Annotated[str, "draft"]:
  return call_llm(f"Write a draft from:\n{brief}")

@step(enable_cache=False)
def evaluate(text: str) -> Annotated[dict, "scores"]:
  return score(text)

@pipeline(dynamic=True)
def review_pipeline(topic: str):
  text = draft(research(topic))
  approved = wait(
      schema=bool,
      question="Approve this draft for evaluation?",
      name="human_approval",
  )
  if approved:
      evaluate(text)

review_pipeline("AI orchestration")

# The run pauses at wait(). Resolve it in the dashboard, or:
#   zenml pipeline runs wait-conditions resolve --run <id> --interactive
#   zenml pipeline runs resume <id>
# Artifacts land in your own bucket, versioned, loadable by name:
Client().get_artifact_version("draft").load()

# Serve it, and every request becomes a tracked run:
#   zenml pipeline deploy run.review_pipeline --name review-svc
Inngest (Python SDK)
import datetime

import inngest

client = inngest.Inngest(app_id="review")

@client.create_function(
  fn_id="review",
  trigger=inngest.TriggerEvent(event="review/requested"),
)
def review(ctx: inngest.ContextSync) -> dict:
  topic = ctx.event.data["topic"]

  brief = ctx.step.run(
      "research", lambda: call_llm(f"Research: {topic}")
  )
  text = ctx.step.run(
      "draft", lambda: call_llm(f"Write a draft from:\n{brief}")
  )

  # Durable wait: releases compute, resumes on the event.
  approval = ctx.step.wait_for_event(
      "await-approval",
      event="review/approved",
      timeout=datetime.timedelta(hours=24),
  )
  if approval is None:
      return {"status": "timed out"}

  return ctx.step.run("evaluate", lambda: score(text))

# Each step.run is memoized, so a retry doesn't repeat
# finished work. The platform drives the function and holds
# the run history.

One orchestrator for
pipelines and agents

If your workflows are event-driven and the hard parts are event subscriptions, fan-out and flow control across more than one language, Inngest is the platform to adopt. If the work is a Python pipeline or agent where you need approvals, retries, sandboxed tool use, and a record of which artifact came from which run, all inside your own infrastructure, ZenML does that on its own. You don’t need Inngest underneath it.