Agentic AI AcademyAgentic AI Academy

Embedding Codex with the SDK

TypeScript and Python SDKs for custom automations

Advanced 13 minBuilder
What you'll be able to do
  • Explain how the Codex SDK wraps the local CLI and why it shares the CLI's session storage and auth
  • Be able to start a thread and run a turn with the TypeScript SDK, reading the result off `turn.finalResponse`
  • Be able to stream intermediate events and resume a persisted thread by its id in both TypeScript and Python
  • Use `outputSchema` to force machine-validated structured output instead of free text
  • Choose and change the sandbox preset (read_only / workspace_write / full_access) per thread or mid-thread
  • Recognize that SDK and CLI versions are coupled and look up current versions from npm, PyPI, and the releases page rather than memorizing them
At a glance

The Codex SDK lets you drive the same Codex agent you use in the terminal from your own TypeScript or Python code — for custom automations, bots, and bespoke CI steps that go beyond what `codex exec` or the GitHub Action give you. The key mental model: the SDK is a thin client over the local Codex CLI; it spawns the same binary, shares the same `~/.codex/sessions` storage, and exchanges structured events with it. This lesson covers the core thread/run/stream/resume API in both languages, structured output, sandbox control, and why SDK and CLI versions are coupled.

  1. 1The mental model: a client over the CLI, not a new agent
  2. 2TypeScript core API: Codex → thread → run
  3. 3Streaming intermediate events
  4. 4Structured output: schema-validated results
  5. 5Resuming threads across runs
  6. 6The Python SDK: same shape, JSON-RPC transport
  7. 7Sandbox presets: WHERE the agent may write
  8. 8Versioning: SDK and CLI move together

The mental model: a client over the CLI, not a new agent

Before any API surface, fix one idea: the Codex SDK does not contain a second agent — it drives the one you already have. When you install @openai/codex-sdk or openai-codex, you are installing a programmatic client that talks to your local Codex CLI. The CLI (a Rust binary) is still the thing that reasons, edits files, runs commands, and enforces the sandbox. The SDK just starts it, hands it your prompt, and reads back structured events.

That single fact explains almost everything about how the SDK behaves:

  • It reuses your existing auth (ChatGPT-account sign-in or API key) and your ~/.codex/config.toml — there is no separate login.
  • It honors your AGENTS.md files, the same way an interactive session does.
  • Its threads are persisted under ~/.codex/sessions — the very same JSONL session store the CLI writes — which is why you can resume them later (and even resume a CLI session's id from the SDK, or vice versa).
  • Because the agent is the CLI, the SDK's capabilities track the CLI's. New CLI features show up to the SDK automatically; this is also why their versions are coupled (see the last section).

Reach for the SDK when codex exec and the GitHub Action stop being enough: when you want to react to intermediate tool calls in code, fan out many threads, wire Codex into a long-running service or chat bot, or build a custom approval/UI layer around the agent.

Key insight

One sentence to remember

The SDK is a remote control for the local Codex CLI: same binary, same auth, same ~/.codex/sessions storage, same AGENTS.md — exposed as objects and async events in your language instead of a terminal UI.

Note

Same wire, two transports

Under the hood the TypeScript SDK spawns the codex CLI and exchanges JSONL events over stdin/stdout, while the Python SDK controls the local Codex app-server over JSON-RPC. Different transport, same agent and same session store.

TypeScript core API: Codex → thread → run

The TypeScript package is @openai/codex-sdk (npm install @openai/codex-sdk), and it requires Node.js 18+. The whole surface is three nouns: a Codex client, a thread (one conversation), and a turn (one run within that conversation).

typescript
import { Codex } from "@openai/codex-sdk";

const codex = new Codex();                  // reuses your CLI auth + config
const thread = codex.startThread();         // a new conversation
const turn = await thread.run("Diagnose the failing test and propose a fix");
console.log(turn.finalResponse);            // the agent's final message

The pieces, with the options that matter:

CallKey optionsPurpose
new Codex({...})apiKey, baseUrl, env, config, codexPathOverrideConfigure the client; codexPathOverride points at a specific codex binary
codex.startThread({...})workingDirectory, model, sandboxMode, skipGitRepoCheckBegin a conversation in a given dir/model/sandbox
thread.run(input, opts?)input = string or InputEntry[]; opts.outputSchemaRun one turn; resolves to a Turn
thread.runStreamed(input)Run a turn and get an async event stream (next section)

Inputs can be a plain string or an array of entries ({type:"text", text} or {type:"local_image", path}), so you can attach screenshots. The result — a Turn — exposes finalResponse (the final assistant message as a string) and items (the steps the agent took). Token usage arrives on the turn.completed event when you stream.

Because the CLI normally requires a Git repo, set skipGitRepoCheck: true when you run against a non-repo directory.

Tip

Leave the model unset to track the default

You can pin model in startThread, but for ChatGPT-account auth OpenAI advises leaving it unset so you automatically follow the recommended default model. Model ids (the gpt-5.x line) rot fast — when you do pin one, look it up at developers.openai.com/codex/models.

Streaming intermediate events

thread.run() resolves only when the whole turn is finished — fine for batch jobs, but useless if you want to show progress, react to a tool call, or surface file changes live. For that, use thread.runStreamed(input), which returns an object with an async event generator you iterate:

typescript
const { events } = await thread.runStreamed("Refactor the auth module and run the tests");

for await (const event of events) {
  if (event.type === "item.completed") {
    console.log("step:", event.item);        // a completed agent step
  }
  if (event.type === "turn.completed") {
    console.log("usage:", event.usage);      // token counts for the turn
  }
}

The stream emits the same family of events the CLI's --json mode emits — this is the contract you parse in automation:

EventCarriesUse it to…
item.completeda completed item (agent message, command execution, file change, …)React to each step the agent finishes
turn.completeda usage object (input / cached / output / reasoning tokens)Track token spend per turn

Think of run() as the convenience wrapper and runStreamed() as the real-time view of the same turn. Use streaming when you're building a UI, a chat bot, or anything that should not freeze until the agent is completely done.

Note

Items are the same vocabulary you already know

An item here is the same shape as in headless --json: types include agent_message, reasoning, command_execution, file_change, mcp_tool_call, and web_search. Learn the event/item schema once and it applies to codex exec --json and both SDKs.

Structured output: schema-validated results

If code is going to consume the agent's answer, you do not want a paragraph of prose — you want JSON that matches a contract. The SDK exposes this through the outputSchema option on run(), the programmatic twin of codex exec --output-schema. You pass a JSON Schema; the final response conforms to it.

typescript
const schema = {
  type: "object",
  properties: {
    summary: { type: "string" },
    status: { type: "string", enum: ["ok", "action_required"] },
  },
  required: ["summary", "status"],
  additionalProperties: false,
} as const;

const turn = await thread.run("Summarize repository status", { outputSchema: schema });
const result = JSON.parse(turn.finalResponse);   // matches the schema above
if (result.status === "action_required") { /* branch on it safely */ }

This is the headless equivalent of "structured outputs": instead of writing brittle regexes against free text, you get a typed object you can branch on. It's the single most important habit for anything machine-consumed — a CI gate, a Slack notifier, a dashboard — because it turns the agent's output into a stable interface rather than a string you hope stays formatted.

Tip

Validate at the boundary

outputSchema constrains what the agent returns, but treat finalResponse as untrusted input anyway: parse it, and re-validate against your schema (e.g. with Zod/Pydantic) before acting. Structured output narrows the failure surface; it doesn't eliminate the need to check.

Resuming threads across runs

A long-running automation rarely lives in one process. You start a thread in one job, the process exits, and a later job needs to continue the same conversation — with all its accumulated context. Because threads persist under ~/.codex/sessions, you only need to carry one thing across the boundary: thread.id.

typescript
// First run — persist the id somewhere durable
const codex = new Codex();
const thread = codex.startThread();
await thread.run("Investigate the CI failure and write a plan");
await saveThreadId(thread.id);              // e.g. to a DB or job output

// Later run — rebuild the thread from its id and continue
const resumed = codex.resumeThread(await loadThreadId());
await resumed.run("Now implement the plan you wrote");

codex.resumeThread(threadId) reconstructs a Thread you lost from memory — its conversation history comes from the session store on disk, not your process. This is exactly how a two-stage "plan, then approve, then apply" workflow is built: stage one writes a plan and saves the id; a human approves; stage two resumes and acts. The session id is the durable handle; everything else is reconstructed.

Key insight

The thread id is your only durable handle

Don't try to serialize the whole conversation yourself — persist thread.id and let resumeThread rehydrate from ~/.codex/sessions. The id is what links a CLI session, a streamed run, and a later resume into one continuous thread.

The Python SDK: same shape, JSON-RPC transport

The Python package is openai-codex (pip install openai-codex) and requires Python 3.10+. Instead of spawning the CLI over stdio, it controls the local Codex app-server over JSON-RPC (and ships a pinned CLI runtime). The object model maps almost one-to-one onto the TypeScript API:

python
from openai_codex import Codex, Sandbox      # package name: openai-codex

with Codex() as codex:
    thread = codex.thread_start(
        model="<model>",                     # look up current ids at developers.openai.com/codex/models
        sandbox=Sandbox.workspace_write,
    )
    result = thread.run("Make a plan to diagnose and fix the CI failures")
    print(result.final_response)             # note the snake_case field

The naming differences are the things to memorize:

ConceptTypeScriptPython
Client classnew Codex()Codex() (sync) · AsyncCodex() (async)
Start a threadcodex.startThread({...})codex.thread_start(model=..., sandbox=...)
Run a turnawait thread.run(input)thread.run(input)
Final answer fieldturn.finalResponseresult.final_response
Sandbox presetSandbox.workspace_write (TS uses sandboxMode)Sandbox.read_only · Sandbox.workspace_write · Sandbox.full_access

Use the with context manager (or AsyncCodex in async code) so the app-server connection is opened and torn down cleanly. Everything you learned about threads, runs, and resuming carries over — only the casing and the transport change.

Note

Sync or async, your choice

Codex is the synchronous client; AsyncCodex is its async/await twin for services and concurrent workloads. Pick the one that matches your runtime — the method names are otherwise the same.

Sandbox presets: WHERE the agent may write

The SDK gives you the same OS-enforced sandbox the CLI does — the boundary that decides WHERE the agent can read, write, and reach the network. (It's independent from approvals, which decide WHEN Codex must ask; that distinction from the CLI lessons holds here too.) There are three presets:

PresetTS (sandboxMode / config)PythonWhat it allows
Read-onlyread-onlySandbox.read_onlyRead files only — no edits, the safe default for analysis/review bots
Workspace-writeworkspace-writeSandbox.workspace_writeRead + write inside the working directory — for tasks that must edit code
Full accessdanger-full-accessSandbox.full_accessNo sandbox boundary — only for trusted, ideally ephemeral environments

Two practical rules:

  • Start narrow. Default to read-only and grant workspace_write only to the threads that genuinely need to edit. Reserve full access for throwaway runners.
  • You can change the sandbox mid-thread. In Python you pass sandbox= to run() to widen (or narrow) permissions on a later turn — e.g. read-only while the agent investigates and writes a plan, then workspace_write once you've approved it. The same WHERE/WHEN model from the interactive CLI applies; the SDK simply lets your code make the call.

Watch out

full_access removes the boundary — scope the key too

full_access / danger-full-access turns off the sandbox entirely; only use it in trusted, ephemeral runners. And remember the agent runs inside your process: scope the API key to the run (e.g. CODEX_API_KEY for the single invocation) so untrusted code in the same environment can't read it.

Key insight

Sandbox (WHERE) ≠ approvals (WHEN)

The sandbox preset controls what the agent is physically allowed to touch. It is a separate axis from approvals (whether Codex pauses to ask). Tightening one doesn't tighten the other — set both deliberately.

Versioning: SDK and CLI move together

Here is the one operational gotcha that bites people. Because each SDK drives the CLI (the TS SDK spawns it; the Python SDK pins a CLI runtime), the SDK and CLI versions are coupled and rev together. A new CLI feature lands, the SDK bumps to expose it, and a mismatched pair can misbehave. So do not hard-code or memorize version numbers — they rot on a weekly cadence. Look them up live:

WhatWhere to check (live)
@openai/codex-sdk (TypeScript) versionhttps://www.npmjs.com/package/@openai/codex-sdk
openai-codex (Python) versionhttps://pypi.org/project/openai-codex/
Codex CLI releases (the coupled binary)https://github.com/openai/codex/releases
Current model ids for model:https://developers.openai.com/codex/models

The same volatility applies to model names. The recommended default is a moving target on the gpt-5.x line — so leave model unset to track it (especially with ChatGPT-account auth), or pin a specific id only after confirming it on the models page. Build your automation to read versions and model availability from these sources, not from a constant in your code.

bash
# Pin both together so they can't drift apart in CI:
npm install @openai/codex-sdk@latest    # then commit the lockfile
codex --version                          # confirm the CLI the SDK will drive

Watch out

Don't memorize numbers — they'll be wrong by next week

Any version number, model id, or default printed in a tutorial (including this one) is a snapshot. Treat npm/PyPI, github.com/openai/codex/releases, and developers.openai.com/codex/models as the source of truth, and pin via your lockfile so the SDK and CLI you tested are the ones that run.

Try it: Drive Codex from code: run, stream, structure, resume

Goal: embed the Codex agent in a tiny script and feel how the SDK maps onto the CLI you already know. 0) Pick a language. TypeScript (npm install @openai/codex-sdk, Node 18+) or Python (pip install openai-codex, Python 3.10+) — both drive your local Codex CLI, so make sure codex --version works and you're signed in (ChatGPT account or API key). 1) Basic run. In a small Git repo, write a script that does new Codex()startThread({ sandboxMode: "read-only" }) (Python: thread_start(sandbox=Sandbox.read_only)) → run("List the three riskiest files in this repo and why"), then print turn.finalResponse / result.final_response. Confirm you get the agent's answer, not a stack trace. 2) Stream it. Switch to runStreamed and log each item.completed plus the usage on turn.completed — watch the agent's steps arrive live. 3) Structure it. Add an outputSchema with { summary: string, status: "ok" | "action_required" }, parse finalResponse, and branch on status. 4) Resume it. Print and save thread.id; in a second run of the script, resumeThread(savedId) and ask it to "expand on the riskiest file" — verify it remembers the prior turn. 5) Widen the sandbox deliberately. Change the preset to workspace_write for a task that edits a file, and note that this is a separate decision from approvals. 6) Write three sentences: where did the SDK reuse your CLI auth/config, what did thread.id + resumeThread let you do across processes, and why is outputSchema better than parsing prose? Bonus: pin the SDK in your lockfile and check the current versions at npmjs.com / pypi.org and github.com/openai/codex/releases.

Key takeaways

  1. 1The Codex SDK is a client over the local Codex CLI — it reuses your auth, config, and AGENTS.md, and persists threads under ~/.codex/sessions; it does not contain a separate agent.
  2. 2TypeScript (`@openai/codex-sdk`, Node 18+) spawns the CLI over JSONL/stdio; Python (`openai-codex`, Python 3.10+) drives the app-server over JSON-RPC — same agent, different transport.
  3. 3Core flow is Codex → startThread/thread_start → run (read finalResponse / final_response); use runStreamed to react to item.completed and turn.completed events in real time.
  4. 4Persist thread.id and call resumeThread to continue a conversation across processes; pass outputSchema/--output-schema to get schema-validated JSON instead of free text.
  5. 5Sandbox presets (read_only | workspace_write | full_access) set WHERE the agent may write, independent of approvals; start narrow and you can widen mid-thread by passing sandbox= to run().
  6. 6SDK and CLI versions are coupled and rot quickly — pin via your lockfile and read current versions/models from npm, PyPI, github.com/openai/codex/releases, and developers.openai.com/codex/models.

Quiz

Lock in what you learned

Check your understanding

0 / 4 answered

1.Your teammate asks, 'If I use the Codex SDK, do I need to set up separate authentication and a separate config from my CLI?' What's the correct answer and why?

2.You're building a CI step that branches on whether Codex thinks action is required. Which approach makes the agent's output safe for code to consume, and what's the trap to avoid?

3.A two-stage automation writes a plan in one job, waits for human approval, then implements it in a later job — a different process. What's the minimal durable handle you must carry between the two jobs?

4.You hard-coded `"@openai/codex-sdk": "0.135.0"` and a specific gpt-5.x model id in your automation six weeks ago, and it's now behaving oddly in CI. What's the most likely root cause and the right fix?

Go deeper

Hand-picked sources to keep learning