All articles
platform engineering · advanced ·

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.

ci-cdcodexcoding-agentspatternssdk

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:

SDK capabilities
CapabilityCLIGitHub ActionSDK
Run prompts in CIYes (codex exec)YesYes
Programmatic approvalNoNoYes
Structured output parsingVia --output-schemaVia inputsNative
Custom orchestration logicNoLimitedFull
Streaming progressLimitedNoYes
Multi-step agent workflowsManualTwo-job splitFull 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.

review-pr.ts typescript
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:

Thread lifecycle
PhaseMethodDescription
Startcodex.threads.start()Creates a new thread with a model and configuration
Runthread.run(prompt)Sends a prompt and processes the agent’s work
Continuethread.continue(prompt)Sends a follow-up prompt on the same thread
Resumethread.resume(threadId)Reconnects to an existing thread by ID
Statusthread.status()Checks if the thread is running, completed, or errored
Cancelthread.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.

multi-stage-pipeline.ts typescript
// 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:

streaming.ts typescript
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
run_agent.py python
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:

Approval policies for CI/CD
PolicyBehaviorUse case
auto-approve-read-onlyAuto-approve Read, Grep, Glob; prompt for writesSafe review and analysis
auto-approve-allApprove every tool call automaticallyFully automated pipelines with trusted prompts
manualEvery tool call waits for approval callbackPipelines with custom approval logic
deny-writesAuto-approve reads, deny all writesAnalysis-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:

structured-review.ts typescript
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

When to use what
ScenarioTool
Simple PR review from a trusted promptCodex CLI codex exec
PR review from forks with untrusted inputopenai/codex-action (security layers)
Custom orchestration with conditional logicTypeScript SDK
Python ecosystem, existing automation scriptsPython SDK / JSON-RPC
Multi-stage pipeline with shared agent contextSDK with persisted thread IDs
Parallel analysis of multiple componentsSDK 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.