Compare

build the agent, orchestrate it as a pipeline

The OpenAI Agents SDK is a lightweight framework for agents, handoffs, and guardrails. ZenML is the orchestration layer around it: versioned artifacts, step caching, human approval, sandboxes, your own cloud.

The OpenAI Agents SDK is a deliberately small framework: agents with instructions and tools, handoffs between them, guardrails on inputs and outputs, and sessions that carry conversation history. It stays out of your way, and for building the agent itself that restraint is the point.

That restraint is also why teams reach for something else once the agent stops being a script. The SDK runs your agent. It doesn’t decide where the run is recorded, what happened to the output, who approved it, or how you avoid paying for the same work twice. ZenML is the orchestration layer that does: your Runner.run() goes inside a @step, and the pipeline around it gives you a run record, versioned artifacts in your own bucket, a cache keyed on the step, its parameters and its inputs, a wait() that pauses the run for a human, a sandbox for the code the agent’s tools execute, and a stack that targets Kubernetes, Vertex AI, SageMaker, AzureML and more.

ZenML

Use ZenML if you are

  • Running agent work on a schedule or behind an endpoint, not from a terminal by hand
  • Asked, weeks later, which output came from which run and which inputs
  • Pausing for a human to approve what the agent produced before the next step acts on it
  • Re-running often enough that skipping unchanged steps saves real money
  • Deploying into your own cloud for security or compliance, with the outputs in your own bucket

OpenAI Agents SDK

Use the OpenAI Agents SDK if you are

  • Building the agent itself: instructions, tools, handoffs between specialised agents
  • Wanting guardrails that validate inputs and outputs as a first-class concept
  • Relying on Sessions to carry conversation history across turns
  • Prototyping quickly, with the smallest possible framework in the way
The SDK runs the agent. ZenML records what the run produced, holds it until someone approves, and makes sure the next run doesn't pay for the parts that didn't change.

Re-running without starting from step one

A multi-step agent pipeline that crashes at the end is expensive to restart. So is tuning the last step while the first three keep re-executing.

OpenAI Agents SDK alone
Crash mid-run, restart from step 1
1research$0.04
2draft$0.09
3reviewcrash
restart from step 1
re-pays $0.13 in tokens already spent
ZenML-wrapped
Retry: completed steps are reused
1@step researchreused
2@step draftreused
3@step reviewre-run
$zenml pipeline runs retry <run>
only the failing step re-bills
  • Cache key: ZenML keys the cache on the step, its parameters and its inputs, plus its output definitions and the artifact store. If nothing changed, you get the stored artifact back and the body never executes. That’s every run, not only after a failure.
  • After a failure: zenml pipeline runs retry <run> re-runs a failed dynamic pipeline run, and the steps that completed are reused rather than re-executed, so the retry doesn’t re-pay for the work above the failure. ZenML retries at step granularity. It won’t resume a step part-way through its body.
  • The agent step: Mark it @step(enable_cache=False) when it must execute every time, and keep the cache on the deterministic steps around it. Add retry=StepRetryConfig(...) for the flaky ones.

Artifacts you can open, not just traces of what happened

The SDK’s tracing shows you the shape of a run: which agent ran, which tools fired, where a handoff happened. That’s the right tool for debugging one run. It isn’t a store of what the run produced.

OpenAI traceWhat happened
model.request218ms
tool.search_docs412ms
model.request186ms
handoff9ms
model.request324ms
Read-only. Reproduce by re-running.
ZenML executionWhat was produced
exec 7c1b3 steps · $0.15
@step researchbrief.json
@step draftdraft.md
@step reviewreview.json
get_artifact_version()runs retrypipeline deploy
  • Artifacts: Every step output is written to your artifact store, versioned, and attached to the run. Name it with Annotated[str, "draft"] and it becomes addressable across runs.
  • Cross-run load: get_artifact_version(...).load() on the ZenML client pulls any earlier run’s output into a notebook or a later pipeline.
  • Serving: zenml pipeline deploy runs the pipeline as a long-running HTTP service, and every request becomes a run with the same artifacts and lineage as a batch job. Self-host the server with Helm, or use ZenML Pro for the managed control plane; the artifacts stay in your bucket either way.

Where the human comes in

An agent that drafts something is easy. An agent whose draft gets acted on is where someone wants to sign off first. In a dynamic pipeline, wait() is that signature:

python
from zenml import pipeline, step, wait

@pipeline(dynamic=True)
def agent_pipeline(topic: str):
    draft = run_agent(gather_context(topic), topic)
    approved = wait(schema=bool, question="Publish this brief?")
    if approved:
        publish(draft)

The run polls for the answer for timeout seconds (600 by default). Nobody answered? The run is marked paused and the orchestration process can be torn down instead of sitting open for days. Someone resolves the condition from the dashboard, the API, or zenml pipeline runs wait-conditions resolve --run <id> --interactive, and zenml pipeline runs resume <run> (or ZenML Pro, automatically) continues from that point with the draft still in the artifact store. The schema can be a Pydantic model, so the approver can hand back edits, not just a yes. on_pause and on_resume hooks on the pipeline are where the Slack message goes.

wait() only works in the pipeline body of a dynamic pipeline, not inside a step. The SDK’s own interruption for tool approval lives inside the run and hands you the state to persist yourself. Different scopes, and they compose.

What ZenML doesn’t do is trace the model and tool calls inside Runner.run(). Want to record those runs and replay them against a prompt or model change? That’s Kitaru, which has a first-class OpenAI Agents SDK adapter: see Kitaru vs OpenAI Agents SDK.

What makes ZenML different

FeatureZenMLOpenAI Agents SDKWhat that means
Versioned artifacts and lineage across runsYesNot supportedStep outputs are stored and loadable by name; the SDK traces a run.
Caching that skips unchanged steps on a re-runYesNot supportedKeyed on the step, its parameters and its inputs.
Pause the run for human approval, resume laterYesPartial support`wait()` in a dynamic pipeline pauses the whole run. The SDK can interrupt a run for tool approval, but you own the storage and the process between turns.
Isolated code execution for tool loopsYesPartial supportZenML's sandbox is a stack component with local, Docker, Kubernetes and Modal flavors. The SDK's hosted code interpreter runs on OpenAI's side, and its beta sandbox agents give an agent a container of its own.
One stack abstraction for Kubernetes, Vertex AI, SageMaker, AzureML and moreYesNot supportedConfigure once, every pipeline uses it.
Serve it and get lineage per requestYesNot supported`zenml pipeline deploy` makes every request a tracked run.
Works with any agent framework, not just oneYesNot supportedA step is ordinary Python.
Agents, tools, and handoffs between specialised agentsNot supportedYesThe SDK's whole point. ZenML doesn't define agents, it runs them.
Input and output guardrailsNot supportedYesNo ZenML equivalent. Keep them inside the step.
Sessions carrying conversation historyNot supportedYesZenML runs aren't conversational.
Tracing of model and tool calls inside the runNot supportedYesZenML records the step, its metadata and its artifacts, not the calls within it.

How the two surfaces map

ConceptOpenAI Agents SDKZenML
Unit of workRunner.run()@step (the agent runs inside it)
BoundaryAn agent and its handoffs@pipeline
Control flowHandoffs between agentsThe pipeline graph, or Python control flow with dynamic=True
ValidationInput and output guardrailsArtifact types on the step signature
Human approvalTool approval inside the run, your own persistencewait() in a dynamic pipeline, resolved from CLI, dashboard or API
Code executionHosted code interpreter, sandbox agents (beta)Sandbox stack component (local, Docker, Kubernetes, Modal)
OutputsRunResult in memoryVersioned artifacts in your bucket
Avoiding repeat workNot applicableenable_cache across runs, zenml pipeline runs retry after a failed dynamic run
ObservabilityTraces of a single runRuns, steps, and artifacts across runs
Where it runsYour processStack orchestrator (zenml stack set)
ServingYour own wrapperzenml pipeline deploy --name ...

Code comparison

ZenML (wrapping the SDK)
from typing import Annotated

from agents import Agent, Runner
from zenml import pipeline, step, wait
from zenml.client import Client

researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a brief.")

@step
def gather_context(topic: str) -> Annotated[str, "context"]:
  return retrieve_documents(topic)

@step(enable_cache=False)
def run_agent(context: str, topic: str) -> Annotated[str, "draft"]:
  result = Runner.run_sync(
      researcher, f"{context}\n\nTopic: {topic}"
  )
  return str(Runner.run_sync(writer, result.final_output).final_output)

@step
def publish(draft: str) -> None:
  post_to_cms(draft)

@pipeline(dynamic=True)
def agent_pipeline(topic: str):
  draft = run_agent(gather_context(topic), topic)
  # Pauses the run until someone answers from the
  # dashboard, the API, or the CLI.
  approved = wait(schema=bool, question="Publish this brief?")
  if approved:
      publish(draft)

agent_pipeline("AI orchestration")

# Edit publish() and re-run: gather_context is cached.
Client().get_artifact_version("draft").load()

# Same code on your cloud:
#   zenml stack set vertex_stack && python agent_pipeline.py
OpenAI Agents SDK alone
from agents import Agent, Runner

researcher = Agent(name="Researcher", instructions="Research the topic.")
writer = Agent(name="Writer", instructions="Write a brief.")

topic = "AI orchestration"
context = retrieve_documents(topic)

result = Runner.run_sync(researcher, f"{context}\n\nTopic: {topic}")
draft = str(Runner.run_sync(writer, result.final_output).final_output)

if input("Publish this brief? ") == "y":
  post_to_cms(draft)

# Tracing shows which agent ran, which tools fired, and where
# the handoff happened. What there isn't: a versioned artifact
# you can load back by name next month, a cache that skips
# retrieval on the next run, an approval that survives the
# process being torn down, or a way to put this on Vertex AI
# without writing the deployment yourself.

Put a pipeline around
your OpenAI agents

Keep the OpenAI Agents SDK for building the agent: the handoffs, the guardrails, the tools. Add ZenML when the agent stops being a script: a human has to approve what it produced, someone will ask next month what a run produced, and it has to run on your own infrastructure with the outputs in a bucket you control.