CI/CD for AI Agents: Running Codex Programmatically with the SDK
How to run Codex agents in automated pipelines using the TypeScript SDK and Python SDK — thread lifecycle, non-interactive mode, structured output, and patterns for embedding agent runs in CI/CD.
The GitHub Actions integration is one way to run Codex in CI. But for teams that need fine-grained control — custom orchestration, conditional logic between agent steps, programmatic response handling — the SDK is the right tool.
Codex offers two SDKs: a TypeScript SDK for Node.js environments and a Python SDK that exposes the agent over JSON-RPC. Both give you full control over thread lifecycle, tool approval, and output handling without wrapping everything in YAML.
What the SDKs give you
The SDKs expose programmatic control over the same engine that powers the CLI. You start threads, send prompts, observe tool calls, approve or deny them, and collect structured output:
| Capability | CLI | GitHub Action | SDK |
|---|---|---|---|
| Run prompts in CI | Yes (codex exec) | Yes | Yes |
| Programmatic approval | No | No | Yes |
| Structured output parsing | Via --output-schema | Via inputs | Native |
| Custom orchestration logic | No | Limited | Full |
| Streaming progress | Limited | No | Yes |
| Multi-step agent workflows | Manual | Two-job split | Full control |
The SDK is for when you need the agent to be a function in a larger pipeline, not the pipeline itself.
TypeScript SDK
Install the TypeScript SDK:
npm install @openai/codex-sdk
The core workflow: start a thread, send a prompt, and process the agent’s response. The agent may request tool approvals — your code decides whether to grant them.
import { Codex } from "@openai/codex-sdk";
const codex = new Codex({
apiKey: process.env.OPENAI_API_KEY,
workingDirectory: process.cwd(),
});
async function reviewPR(options: {
baseSha: string;
headSha: string;
prNumber: number;
}) {
const thread = await codex.threads.start({
model: "cwg-2.2",
});
const result = await thread.run(
`Review PR #${options.prNumber}.
Diff: git diff ${options.baseSha}..${options.headSha}
Report bugs, security issues, and suggestions.
Format as a structured code review with sections: Summary, Issues Found, Suggestions.`,
{
onToolUse: async (toolCall) => {
// Auto-approve safe read-only tools
if (["Read", "Grep", "Glob", "Bash"].includes(toolCall.name)) {
return { decision: "approve" };
}
// Deny anything that mutates
return {
decision: "deny",
reason: `Tool ${toolCall.name} is not allowed in CI review mode`,
};
},
}
);
return result.finalMessage;
}
// Run it
const review = await reviewPR({
baseSha: process.env.BASE_SHA!,
headSha: process.env.HEAD_SHA!,
prNumber: parseInt(process.env.PR_NUMBER!),
});
console.log(review); Thread lifecycle
A thread follows a predictable lifecycle. Understanding each phase matters when you’re running agents in automated pipelines:
| Phase | Method | Description |
|---|---|---|
| Start | codex.threads.start() | Creates a new thread with a model and configuration |
| Run | thread.run(prompt) | Sends a prompt and processes the agent’s work |
| Continue | thread.continue(prompt) | Sends a follow-up prompt on the same thread |
| Resume | thread.resume(threadId) | Reconnects to an existing thread by ID |
| Status | thread.status() | Checks if the thread is running, completed, or errored |
| Cancel | thread.cancel() | Stops a running thread |
The difference between continue and resume matters. continue adds to an active thread you already have a reference to. resume reconnects to a thread you’ve lost the reference to — useful when the agent run spans multiple CI jobs or when you persist thread IDs between pipeline stages.
// Stage 1: Generate tests
const thread = await codex.threads.start({ model: "cwg-2.2" });
const testGen = await thread.run(
"Generate unit tests for the changed files in this PR."
);
// Persist the thread ID for the next stage
await fs.writeFile(".codex-thread-id", thread.id);
// Stage 2 (separate process or job): Review the generated tests
const threadId = await fs.readFile(".codex-thread-id", "utf-8");
const resumed = await codex.threads.resume(threadId);
const review = await resumed.continue(
"Review the tests you just generated. Are they thorough? Any edge cases missed? Improve them."
); Streaming progress
For long-running CI jobs, you want visibility into what the agent is doing. The SDK supports streaming:
const thread = await codex.threads.start({ model: "cwg-2.2" });
for await (const event of thread.stream(prompt)) {
switch (event.type) {
case "thinking":
console.log(`Agent thinking: ${event.content.substring(0, 100)}...`);
break;
case "tool_call":
console.log(`Running tool: ${event.toolName}`);
break;
case "tool_result":
console.log(
`Tool ${event.toolName} completed (${event.duration_ms}ms)`
);
break;
case "message":
console.log(`\nAgent response:\n${event.content}\n`);
break;
case "error":
console.error(`Error: ${event.message}`);
break;
}
} Streaming gives you progress visibility without parsing log files. Each event type is a typed object — no regex matching against stdout needed.
Python SDK
The Python SDK takes a different approach. Instead of importing a library and calling methods, you run the Codex app server and communicate over JSON-RPC:
pip install codex-app-server
import subprocess
import json
def run_codex_prompt(prompt: str, working_dir: str = ".") -> str:
"""Run a Codex prompt and return the final message."""
server = subprocess.Popen(
["codex-app-server"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=working_dir,
)
request = json.dumps({
"jsonrpc": "2.0",
"method": "threads.run",
"params": {
"model": "cwg-2.2",
"prompt": prompt,
"approval_policy": "auto-approve-read-only",
},
"id": 1,
})
stdout, stderr = server.communicate(input=request + "\n", timeout=300)
if server.returncode != 0:
raise RuntimeError(f"Codex server failed: {stderr}")
response = json.loads(stdout)
return response["result"]["finalMessage"]
# Usage in CI
review = run_codex_prompt(
"Review the diff between main and this branch for issues.",
working_dir="/home/runner/work/my-repo",
)
print(review) Python thread management
The JSON-RPC interface supports the same thread lifecycle:
# Start a thread
start_resp = call_server({
"jsonrpc": "2.0",
"method": "threads.start",
"params": {"model": "cwg-2.2"},
"id": 1,
})
thread_id = start_resp["result"]["id"]
# Send a prompt
run_resp = call_server({
"jsonrpc": "2.0",
"method": "threads.run",
"params": {
"thread_id": thread_id,
"prompt": "Analyze the codebase for security vulnerabilities.",
},
"id": 2,
})
# Continue the thread
continue_resp = call_server({
"jsonrpc": "2.0",
"method": "threads.continue",
"params": {
"thread_id": thread_id,
"prompt": "For each vulnerability found, suggest a specific fix.",
},
"id": 3,
})
Non-interactive mode
Both SDKs support non-interactive mode — the agent runs without pausing for human input. This is essential for CI/CD. Configure it through the approval_policy parameter:
| Policy | Behavior | Use case |
|---|---|---|
auto-approve-read-only | Auto-approve Read, Grep, Glob; prompt for writes | Safe review and analysis |
auto-approve-all | Approve every tool call automatically | Fully automated pipelines with trusted prompts |
manual | Every tool call waits for approval callback | Pipelines with custom approval logic |
deny-writes | Auto-approve reads, deny all writes | Analysis-only pipelines |
For most CI use cases, auto-approve-read-only hits the right balance. The agent can explore the codebase freely but can’t make changes unless you explicitly allow it through the approval callback.
Structured output
When the agent’s response feeds into another system — posting a PR comment, creating a Linear issue, updating a dashboard — you need structured output. Both SDKs support output schemas:
const review = await thread.run(
`Review the PR diff and report findings.`,
{
outputSchema: {
type: "object",
properties: {
summary: { type: "string" },
severity: { type: "string", enum: ["clean", "minor", "major", "critical"] },
issues: {
type: "array",
items: {
type: "object",
properties: {
file: { type: "string" },
line: { type: "number" },
severity: { type: "string" },
description: { type: "string" },
suggestion: { type: "string" },
},
required: ["file", "description", "suggestion"],
},
},
},
required: ["summary", "severity", "issues"],
},
}
);
// review.output is a typed object, not a string
if (review.output.severity === "critical") {
await blockPRMerge(prNumber);
}
for (const issue of review.output.issues) {
await createInlineComment(prNumber, issue.file, issue.line, issue.suggestion);
} Structured output turns the agent from a text generator into a data producer. Downstream steps consume typed objects, not parsed strings.
Patterns for embedding agent runs
Pattern 1: Independent analysis thread
Run the agent on a fresh thread per CI event. No shared state between runs. Each analysis is self-contained:
# GitHub Actions
- name: Code Review
run: npx tsx scripts/codex-review.ts
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
Pattern 2: Persisted thread across pipeline stages
Start a thread, persist the ID, and continue it in later pipeline stages. Useful when later stages need context from earlier ones:
// Stage 1: Plan
const thread = await codex.threads.start({ model: "cwg-2.2" });
await thread.run("Create an implementation plan for this feature.");
await fs.writeFile(".codex-thread-id", thread.id);
// Stage 2: Implement (separate job, reads the thread ID)
const threadId = await fs.readFile(".codex-thread-id", "utf-8");
const resumed = await codex.threads.resume(threadId);
await resumed.continue("Implement the plan you created in the previous stage.");
Pattern 3: Parallel agent fan-out
For work that can be done in parallel — reviewing multiple services, generating tests for separate modules — fan out to multiple threads:
const services = ["api", "web", "workers", "db-migrations"];
const reviews = await Promise.all(
services.map(async (service) => {
const thread = await codex.threads.start({ model: "cwg-2.2" });
return thread.run(
`Review the changes in the services/${service} directory for issues.`
);
})
);
// Aggregate results
const allIssues = reviews.flatMap((r) => r.output?.issues ?? []);
await createAggregatedReviewComment(prNumber, allIssues);
Choosing the right tool
| Scenario | Tool |
|---|---|
| Simple PR review from a trusted prompt | Codex CLI codex exec |
| PR review from forks with untrusted input | openai/codex-action (security layers) |
| Custom orchestration with conditional logic | TypeScript SDK |
| Python ecosystem, existing automation scripts | Python SDK / JSON-RPC |
| Multi-stage pipeline with shared agent context | SDK with persisted thread IDs |
| Parallel analysis of multiple components | SDK with fan-out |
The CLI is simplest. The GitHub Action is safest for untrusted input. The SDK is most flexible. Choose based on how much control you need over what the agent does and how its output is consumed.
SDKs give you programmatic control over agent behavior
Thread lifecycle, tool approval callbacks, streaming progress, and structured output parsing — the SDKs expose every part of the agent loop as typed APIs rather than string parsing.
Tool approval is your CI security boundary
The onToolUse callback lets you auto-approve reads, conditionally allow writes, and deny everything else. More granular than sandbox levels alone.
Persist thread IDs across pipeline stages
Write the thread ID to a file between pipeline stages. Later stages resume the thread with full context rather than starting fresh.
Structured output makes agents composable
Output schemas turn agent responses into typed objects. Downstream steps consume data, not parsed text.
Choose based on control needs
CLI for simple runs. GitHub Action for untrusted input. SDK for custom orchestration, conditional logic, and multi-stage workflows.