HomeJournalThis post

OpenAI Agents SDK vs LangGraph

A matched production workflow compares OpenAI Agents SDK and LangGraph across checkpoints, approvals, duplicate side effects, tracing, operations, and migration.

JP
JP Casabianca
UI/UX designer and full-stack engineer · Bogotá

OpenAI Agents SDK vs LangGraph should be compared with one identical workflow, not a feature checklist. The useful test invokes both current runtimes, pauses for approval, fails after one external effect, resumes, and leaves exactly one provider receipt.

This comparison pins the JavaScript packages and uses a deterministic model response only to remove model variance. It distinguishes built-in interruption and resume guarantees from application-owned storage, approval policy, and side-effect idempotency.

Actual Agents SDK and LangGraph interruption-resume workflowAgents SDK RunState and a LangGraph checkpoint each pause before approval, then an application idempotency receipt prevents a second effect after an injected crash. proposeinterruptapproveeffect + crashresumeRunStatecheckpointapp receipt K2
Figure 1: Framework state resumes orchestration; the application-owned K2 receipt reconciles the external effect after failure.

Benchmark OpenAI Agents SDK vs LangGraph

OpenAI Agents SDK vs LangGraph becomes useful when both current JavaScript runtimes execute the same work: propose a refund, pause for approval, perform one idempotent side effect, survive a failure after that effect, and emit an auditable terminal receipt. The harness imports @openai/agents 0.16.0 and @langchain/langgraph 1.4.10. A deterministic Agents SDK test model removes model variance; it does not mock either orchestration runtime or support timing claims.

The benchmark workflow proposes a refund, pauses for approval, performs one idempotent side effect, and resumes after an injected failure. Both implementations use their actual JavaScript orchestration runtime with the same amount, key, decision, and failure point. The deterministic Agents SDK test model removes model variance without replacing the Agents SDK runner.

Runnable artifact: The pinned actual Agents SDK and LangGraph runtimes each interrupt, resume, survive one injected post-effect failure, and reconcile exactly one provider receipt.

Save this proof as agent-runtime-parity.test.mjs and run node agent-runtime-parity.test.mjs. Expected final line: PASS: actual runtimes preserve one effect.

import assert from "node:assert/strict";
import { Agent, RunState, run, setTracingDisabled, tool } from "@openai/agents";
import { ScriptedModel, assistantMessage, functionCall } from "@openai/agents/testing";
import { Command, END, MemorySaver, START, StateGraph, StateSchema, interrupt } from "@langchain/langgraph";
import { z } from "zod";
setTracingDisabled(true);
const packageVersions={"@openai/agents":"0.16.0","@langchain/langgraph":"1.4.10","@langchain/core":"1.2.8","zod":"4.4.3"};
async function runAgents(){
  const effects=new Map(); let crash=true;
  const refund=tool({name:"issue_refund",description:"Issue one idempotent refund",parameters:z.object({key:z.string(),amount:z.number()}),needsApproval:true,errorFunction:null,execute:async({key,amount})=>{const receipt=effects.get(key)??"receipt:"+key+":"+amount;effects.set(key,receipt);if(crash){crash=false;throw Error("crash-after-effect")}return receipt;}});
  const model=new ScriptedModel([[functionCall("issue_refund",{key:"K2",amount:35},{callId:"call-1"})],[assistantMessage("Refund recorded once.")]]);
  const agent=new Agent({name:"Refund agent",instructions:"Use issue_refund.",tools:[refund],model});
  let result=await run(agent,"Refund 35 with K2");
  assert.equal(result.interruptions.length,1); assert.equal(effects.size,0);
  let state=await RunState.fromString(agent,result.state.toString());
  state.approve(state.getInterruptions()[0]);
  let crashes=0; try{await run(agent,state)}catch(error){assert.match(String(error),/crash-after-effect/);crashes++;}
  state=await RunState.fromString(agent,state.toString()); result=await run(agent,state);
  assert.equal(effects.size,1); assert.equal(crashes,1);
  return {runtime:"OpenAI Agents SDK",interruptions:1,effectsBeforeApproval:0,injectedCrashes:crashes,providerEffects:effects.size,terminal:"completed"};
}
async function runLangGraph(){
  const State=new StateSchema({key:z.string(),amount:z.number(),status:z.string(),receipt:z.string().nullable()});
  const effects=new Map(); let crash=true;
  const graph=new StateGraph(State)
    .addNode("approval",state=>{const approved=interrupt({key:state.key,amount:state.amount});return new Command({goto:approved?"effect":"rejected",update:{status:approved?"approved":"rejected"}});},{ends:["effect","rejected"]})
    .addNode("effect",state=>{const receipt=effects.get(state.key)??"receipt:"+state.key+":"+state.amount;effects.set(state.key,receipt);if(crash){crash=false;throw Error("crash-after-effect")}return {status:"completed",receipt};})
    .addNode("rejected",()=>({status:"rejected"})).addEdge(START,"approval").addEdge("effect",END).addEdge("rejected",END)
    .compile({checkpointer:new MemorySaver()});
  const config={configurable:{thread_id:"refund-K2"}};
  const first=await graph.invoke({key:"K2",amount:35,status:"proposed",receipt:null},config);
  assert.equal(first.__interrupt__.length,1); assert.equal(effects.size,0);
  let crashes=0; try{await graph.invoke(new Command({resume:true}),config)}catch(error){assert.match(String(error),/crash-after-effect/);crashes++;}
  const final=await graph.invoke(null,config);
  assert.equal(final.status,"completed"); assert.equal(effects.size,1); assert.equal(crashes,1);
  return {runtime:"LangGraph",interruptions:1,effectsBeforeApproval:0,injectedCrashes:crashes,providerEffects:effects.size,terminal:final.status};
}
const receipt={packageVersions,workflow:"approval-crash-resume",agents:await runAgents(),langGraph:await runLangGraph(),ownership:{checkpoints:"framework serialization/checkpointer",storage:"application chooses production persistence",externalEffects:"application idempotency key plus provider receipt"}};
assert.deepEqual([receipt.agents.providerEffects,receipt.langGraph.providerEffects],[1,1]);
console.log(JSON.stringify(receipt,null,2)); console.log("PASS: actual runtimes preserve one effect");

Map each runtime's interruption boundary.

The OpenAI Agents SDK HITL guide documents needsApproval tools, interruption items, approve or reject decisions, serialized RunState, and resuming a runner from that state. LangGraph interrupts documents interrupt(), Command resume, thread identity, and checkpointers. Both frameworks provide pause-resume mechanics; neither makes an external refund exactly-once by itself.

OpenAI Agents SDK provides built-in tool approvals, interruption items, approve or reject decisions, serializable RunState, and resume through its runner. LangGraph provides graph state, interrupt(), Command resume, thread identity, and checkpointers. Those are framework primitives; durable database selection, operator identity, approval policy, and effect reconciliation remain application design.

Resume the exact interrupted proposal

The Agents SDK run returns interruptions plus a resumable RunState; the harness serializes that state, restores it, approves the exact tool call, and runs again. LangGraph checkpoints the graph at interrupt(), then resumes the same thread with Command({ resume: true }). Human identity, policy version, approval expiry, durable storage selection, and authorization for the real business effect remain application responsibilities around those framework primitives.

The executable checks zero effects before approval and a crash after the provider records the effect but before workflow completion. Agents SDK RunState and a LangGraph MemorySaver checkpoint each resume orchestration; the shared provider idempotency key prevents a second refund. MemorySaver is a test fixture, not evidence of production durability.

BoundaryAgents SDK 0.16LangGraph 1.4Application owns
PauseneedsApproval interruptioninterrupt()Approval policy
ResumeSerialized RunStateCommand + threadDurable store
Test stateRunState stringMemorySaver fixtureProduction backend
EffectTool executesEffect node executesK2 + provider receipt
Figure 2: Current framework primitives and application obligations are kept in separate columns.

Crash around the side-effect boundary.

The dangerous window lies after an external effect succeeds but before local completion is durable. Both workflows use an idempotency key and queryable effect receipt, then restart at that cut point. OpenAI Agents SDK vs LangGraph therefore compares framework recovery only after the application supplies the same reconciliation seam. Durable AI agent execution supplies the underlying checkpoint discipline, while agent compensation covers cases where reversal is required.

Human approval carries the exact proposed arguments, evidence summary, policy version, expiry, and actor identity. Editing an argument invalidates the old approval and creates a new proposal hash. Both runtimes can host that contract, but their natural checkpoint and interrupt boundaries change how much glue the application must own and audit.

Normalize traces without discarding evidence

A common event vocabulary makes proposal, approval, tool attempt, tool result, checkpoint, and terminal state comparable. Native traces are retained beside normalized ones to reveal what the adapter loses. Multi-agent causal trace testing shows why event order and identity matter more than a screenshot of a successful final answer.

Tracing parity uses normalized facts for interruption count, effects before approval, injected crash count, provider effect count, and terminal state. Framework-native traces remain useful, but the artifact neither claims they are equivalent nor compares overhead. Normalization is a receipt for the shared business contract, not a substitute for native diagnostic evidence.

Read the matched refund receipt step by step. The proposal requests a 35 refund under idempotency key K2. In the Agents SDK lane, needsApproval produces one interruption and zero provider effects; the harness serializes RunState, restores it, approves the interruption, then injects a crash after the tool records the provider receipt. In the LangGraph lane, interrupt() yields one interruption under a stable thread ID; Command resume reaches the effect node and the same crash point. Each retry rediscovers K2 and finishes with exactly one provider effect.

That result is deliberately narrow. It proves actual runtime interruption and resume APIs were executed and that application-owned idempotency closes the ambiguous side-effect window. It does not prove the in-memory test storage is production durable, that either framework is faster, or that a provider lacking idempotency can be made exactly-once by checkpointing alone.

Price persistence and migration honestly

RunState serialization and LangGraph checkpointers establish framework resume boundaries, but applications still choose a production store, retention policy, encryption, schema evolution, and operator access. The executable uses LangGraph MemorySaver only as an isolated test fixture; production requires a persistent checkpointer. Any OpenAI Agents SDK vs LangGraph migration must also preserve pending approvals and independent provider receipts. Agent context compaction audits provides an adjacent example of retained state as a versioned migration surface.

The migration-cost map counts RunState or checkpoint conversion, pending approval transfer, trace continuity, tool wrapper changes, persistent-store changes, operational training, and rollback coexistence. Rewriting a happy-path graph is a small fraction of switching live work. Any recommendation that ignores in-flight state and independent effect receipts understates the real cost.

Reopen the runtime decision at named thresholds. A recommendation expires when workflow topology, provider mix, checkpoint volume, approval load, hosting constraints, or team ownership changes materially. OpenAI Agents SDK vs LangGraph evidence includes thresholds: more than a declared number of explicit pause nodes, a need to inspect and edit graph state operationally, or a shift away from an OpenAI-centered model stack triggers a fresh comparison. Conversely, graph machinery that becomes mostly unused can reopen a decision toward a smaller loop.

The architecture record avoids predicting future framework winners. It preserves what was measured, why the current candidate fit, what glue remained, and which migration surfaces would be expensive. OpenAI Agents SDK vs LangGraph is then a revisable engineering decision with a stable benchmark, not a permanent identity the team must defend after its workload has changed.

  1. 1Interrupt

    One pause; zero effects

  2. 2Approve

    Resume exact proposal

  3. 3Crash

    Provider stores K2 once

  4. 4Recover

    One terminal completion

Figure 3: Both actual runtimes pass the same failure-injected business receipt.

Choose by explicit ownership constraints

My recommendation is conditional. Prefer the Agents SDK when its agent loop, built-in tool approval, serialized RunState, sessions, and tracing match an OpenAI-centered application with modest topology. Prefer LangGraph when explicit nodes, edges, checkpointed threads, and multiple interruption points are the operating model. In either case, own durable storage configuration and effect receipts explicitly; the measured parity is one approved refund, one injected crash, one provider effect, and one terminal completion—not a universal durability or speed ranking.

My default is the smaller ownership surface that still satisfies the tested recovery contract. An OpenAI-centered dynamic loop may value the Agents SDK's direct runner and built-in HITL; a workflow with explicit topology and many interruption nodes may value LangGraph's graph and checkpointer model. Both still need an application-owned persistence choice and side-effect ledger.

Exercise operator recovery without a notebook. A production drill starts from a workflow identity and gives the operator only supported tools. For Agents SDK, application storage must locate the serialized RunState and its approval record; for LangGraph, a persistent checkpointer must locate the thread checkpoint and interrupt. Both paths also locate the independent provider effect receipt before permitting recovery. Framework state explains where orchestration paused, while the application ledger explains whether the consequential business effect already happened. If those records cannot be reconciled without the original author, the workflow is not production-ready.

Keep a runtime-switch receipt

The final receipt contains exact package versions, actual framework imports, workflow fixture, interruption count, effects before approval, injected failure count, provider effect count, terminal state, and ownership notes. Re-run it on upgrades and new pause points. It deliberately omits comparative latency because the deterministic model fixture and in-memory checkpointer cannot establish production speed or durability. A runtime remains suitable only while operators can explain which guarantee comes from the framework and which comes from application infrastructure.

Agent orchestration frameworks should be compared through one matched failure drill. Durable agent workflows make checkpoints, approvals, and side-effect reconciliation observable, so this agent framework comparison measures recovery ownership instead of counting convenience APIs.

The release receipt includes pinned versions, actual runtime imports, interruption counts, injected failure, provider effect count, terminal result, and explicit ownership notes. It does not report framework timing or production durability from deterministic model responses and in-memory checkpoints. I rerun it on dependency upgrades and whenever a new pause or business-effect boundary appears.

OpenAI Agents SDK vs LangGraph is a workload and ownership decision. Keep the OpenAI Agents SDK vs LangGraph fixture alive so framework upgrades, new interrupts, and changed recovery duties can reopen the choice with evidence.