Skip to content

Run Codex with the OpenAI Agents API in a sandbox

Last updated View as MarkdownAgent setup

The OpenAI Agents API ↗︎ runs the Codex agent loop at OpenAI. With a self-hosted environment, Codex runs commands and edits files in your Cloudflare account instead. The Cloudflare template runs the Codex executor of each session in its own Linux sandbox: a Container that one Durable Object starts for that session.

Architecture showing an application creating an OpenAI task, webhooks starting a Cloudflare container, and the application fetching the result

The template ↗︎ contains the Worker and the container image that this guide deploys.

How it works

OpenAI sends signed webhooks to a Worker in your account as a session starts, needs an environment, works, and goes idle. The Worker verifies each webhook, retrieves the session from OpenAI, and passes it to the Durable Object named for that session. The Durable Object starts a container that runs codex exec-server. The executor connects out to OpenAI with a restricted API key. It runs commands from the agent against files in /workspace, which stay in your Cloudflare account.

Prerequisites

You need:

  • A Cloudflare account on the Workers Paid plan with access to Containers
  • OpenAI Agents API access and an OpenAI API key
  • curl
  • For manual deployment, Node.js 24 ↗︎ or later and a running Docker ↗︎ daemon

You also need a restricted OpenAI API key for codex exec-server, called the executor key in this guide. It needs the api.model.read and api.agents.environments.connect permissions. The OpenAI API key that the Worker uses needs api.agents.read. Both keys must belong to the same organization, project, and owner, whether that owner is a user or a service account.

Deploy the template

These steps create an OpenAI agent, deploy the Worker and its container with the Deploy to Cloudflare button, register the webhook, and run a test task in /workspace.

  1. Set your OpenAI API key, then create an agent:
export OPENAI_API_KEY="<OPENAI_API_KEY>"
curl "https://api.openai.com/v1/agents" \
	--request POST \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY" \
	--json '{
		"name": "sandbox-demo",
		"model": "gpt-5.6-sol"
	}'

Copy the id field from the response and save it as the agent ID:

export OPENAI_AGENT_ID="agent_..."
  1. Generate a shared secret for the container cleanup endpoint, and save it:

    openssl rand -hex 32

    Select Deploy to Cloudflare:

    Deploy to Cloudflare

    Enter these values when prompted:

    Variable Value
    OPENAI_API_KEY The OpenAI key used to retrieve session state
    OPENAI_EXECUTOR_API_KEY The restricted executor key
    OPENAI_AGENT_ID The agent ID from step 1
    OPENAI_WEBHOOK_SECRET pending-webhook-registration for the first deployment
    EXECUTOR_CLIENT_SECRET The shared secret from this step

    Save the deployed Worker URL:

    export WORKER_URL="https://<YOUR_WORKER>.workers.dev"

    The template starts a container as soon as OpenAI creates a session, and snapshots the container when the session goes idle. An idle session keeps its container for EXECUTOR_KEEP_ALIVE_SECONDS, which wrangler.jsonc in the template sets to 30 seconds.

  2. In OpenAI project webhook settings ↗︎, register https://<YOUR_WORKER>.workers.dev/webhook. OpenAI must be able to reach this URL.

    Subscribe to these events:

    • agent.session.created
    • agent.session.action_required
    • agent.session.in_progress
    • agent.session.idle
    • agent.session.failed

    Copy the signing secret that OpenAI returns. In Settings > Variables and Secrets for the Worker, replace OPENAI_WEBHOOK_SECRET with it, then select Deploy. If you deployed manually, run this command in the openai/agents-api directory instead:

    npx wrangler secret put OPENAI_WEBHOOK_SECRET

    Check the setup:

    curl --fail-with-body "$WORKER_URL/health"

    The Worker is ready when the response contains "configured": true and "webhook_configured": true.

  3. Create a self-hosted session:

curl "https://api.openai.com/v1/agents/sessions" \
	--request POST \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY" \
	--json '{
		"agent_id": "$OPENAI_AGENT_ID",
		"environment": {
				"type": "self_hosted",
				"workspace_directory": "/workspace"
		}
	}'

Copy the id field from the response and save it as the session ID:

export SESSION_ID="sess_..."

Open the session event stream in one terminal:

curl --no-buffer \
  "https://api.openai.com/v1/agents/sessions/$SESSION_ID/events" \
  --header "OpenAI-Beta: agents=v1" \
  --header "Authorization: Bearer $OPENAI_API_KEY" \
  --header "Accept: text/event-stream"

While the stream is open, submit a task from another terminal:

curl "https://api.openai.com/v1/agents/sessions/$SESSION_ID/events" \
	--request POST \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY" \
	--json '{
		"events": [
				{
						"type": "session.input.message",
						"input": [
								{
										"role": "user",
										"content": [
												{
														"type": "input_text",
														"text": "Use the shell to write hello to /workspace/hello.txt, then read it."
												}
										]
								}
						]
				}
		]
	}'

The event stream shows the progress of the agent and its reply.

Deploy manually

To deploy from your terminal instead of with the button in step 2:

  1. Clone the template repository, install its dependencies, and log in to Cloudflare:

    git clone https://github.com/cloudflare/sandbox-sdk.git
    cd sandbox-sdk/openai/agents-api
    npm install
    npx wrangler login
  2. Generate a shared secret for the container cleanup endpoint, and save it:

    openssl rand -hex 32
  3. Store the Worker secrets. Enter your OpenAI API key, the executor key, the agent ID, and the shared secret when prompted:

    npx wrangler secret put OPENAI_API_KEY
    npx wrangler secret put OPENAI_EXECUTOR_API_KEY
    npx wrangler secret put OPENAI_AGENT_ID
    npx wrangler secret put EXECUTOR_CLIENT_SECRET
  4. Deploy the Worker and its container:

    npm run deploy

To change the keep-alive, prewarm, and snapshot settings, edit EXECUTOR_KEEP_ALIVE_SECONDS, EXECUTOR_PREWARM_ENABLED, and EXECUTOR_SNAPSHOTS_ENABLED in wrangler.jsonc, then run npm run deploy again.

Save the deployed Worker URL, then continue with step 3.

Reconnect an existing session

Open the session event stream again, then send more input:

curl "https://api.openai.com/v1/agents/sessions/$SESSION_ID/events" \
	--request POST \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY" \
	--json '{
		"events": [
				{
						"type": "session.input.message",
						"input": [
								{
										"role": "user",
										"content": [
												{
														"type": "input_text",
														"text": "Read /workspace/hello.txt again."
												}
										]
								}
						]
				}
		]
	}'

Build an application on the Agents API

The basic example ↗︎ in the template is a TypeScript application that uses the OpenAI Agents API TypeScript SDK. It creates self-hosted sessions that run on the executor Worker. Its HTTP endpoints send the first input, send follow-up input, and clean up. The POST /demo endpoint runs the whole workflow: it creates a session, writes and reads a file in the container, sends a follow-up message, and then deletes the OpenAI session and its executor.

Clean up

Delete the OpenAI session:

curl "https://api.openai.com/v1/agents/sessions/$SESSION_ID" \
	--request DELETE \
	--header "OpenAI-Beta: agents=v1" \
	--header "Authorization: Bearer $OPENAI_API_KEY"

To stop the container of a session at once, call the cleanup endpoint with the shared secret from deployment:

export WORKER_URL="https://<YOUR_WORKER>.workers.dev"
export EXECUTOR_CLIENT_SECRET="<EXECUTOR_CLIENT_SECRET>"

curl --fail-with-body \
  --request DELETE \
  --header "Authorization: Bearer $EXECUTOR_CLIENT_SECRET" \
  "$WORKER_URL/executors/$SESSION_ID"

Deleting an OpenAI session does not send a webhook to the Worker. The Worker stops a container and deletes its record of the container snapshot when it receives an agent.session.failed webhook, when a session lookup returns 404 Not Found, or when you call the cleanup endpoint. An idle session that still exists keeps its snapshot for its next environment connection.

Session lifecycle

  1. The application creates or retrieves an OpenAI session and sends input through the Agents API.

  2. When EXECUTOR_PREWARM_ENABLED is true, as in the template, an agent.session.created webhook makes the Worker retrieve the session and start its container with the environment ID and remote URL of the session.

  3. An agent.session.action_required webhook makes the Worker retrieve the session, confirm that the configured agent owns it, and read the environment ID and remote URL that the session needs.

  4. The Durable Object named for the session starts a container with those connection details and the executor key. codex exec-server connects out to OpenAI.

  5. Each container start, environment connection, and agent.session.in_progress webhook resets a deadline of EXECUTOR_KEEP_ALIVE_SECONDS. When the deadline passes, the Worker retrieves the session. If the session is still active, the Worker sets a new deadline.

  6. An agent.session.idle webhook makes the Worker snapshot the container and reset the deadline. When the deadline passes, the Worker stops the container and keeps the snapshot.

  7. New input sends another agent.session.action_required webhook. The Worker reuses a running container for the same environment ID, or starts the next environment from the saved snapshot.

Lifecycle showing an application creating an Agents API session, OpenAI sending webhooks to Cloudflare, and the container connecting its Codex executor to OpenAI

Keep the workspace between turns

When a session goes idle, the Worker snapshots the container filesystem before it stops the container. The next environment connection starts from that snapshot, so the files in /workspace come back. Running processes and memory do not. If the snapshot fails, the Worker leaves the container running and schedules another lifecycle check. For more information, refer to Sandbox lifetime.

A session uses its snapshot only while the session lasts. The Worker deletes the snapshot record of a session when the session fails, when a later lookup finds that the session no longer exists, or when you call the cleanup endpoint. The next session then starts from the image. The Worker does not delete the snapshot itself.

With EXECUTOR_SNAPSHOTS_ENABLED set to false, each new executor starts with an empty /workspace. To keep files after a session ends, store them somewhere else, such as an R2 bucket. For more information, refer to Mount an R2 bucket.

Add tools to the container

The openai/agents-api/Dockerfile file in the template defines the executor image. Add Debian packages to its apt-get install command. For example, to add jq and Python:

RUN apt-get update \
    && apt-get install --yes --no-install-recommends \
      ca-certificates \
      curl \
      git \
      jq \
      python3 \
      ripgrep \
    && rm -rf /var/lib/apt/lists/*

You can also install language tools in the image, such as global npm packages. Do not put API keys or other secrets in the Dockerfile. Pass them at runtime through Worker secrets and container environment variables.

To build and deploy the new image, run npm run deploy in the openai/agents-api directory. New containers start from it. A session that starts from a snapshot keeps the filesystem of its old image, so it does not get the new packages.

Security considerations

The template is a minimal example. Review these defaults before you adapt it for production:

  • Your OpenAI API key, the webhook secret, and EXECUTOR_CLIENT_SECRET stay in Worker secrets. The executor key goes into the container as CODEX_API_KEY, where every process in the container can read it. Give the executor key only the two permissions it needs.
  • The container has Internet access so that codex exec-server can reach OpenAI. Every command the agent runs has the same access. To restrict destinations or add credentials to requests from your Worker, refer to Outbound traffic.
  • OpenAI must reach /webhook without an interactive Access login. The Worker checks the signature on each webhook, and the cleanup endpoint requires EXECUTOR_CLIENT_SECRET. If you protect other routes with Cloudflare Access, use path-specific policies that leave /webhook reachable.

For more information about what a sandbox exposes to the code inside it, refer to Sandbox security.

Was this helpful?