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.
The template ↗︎ contains the Worker and the container image that this guide deploys.
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.
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.
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.
- 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_..."-
Generate a shared secret for the container cleanup endpoint, and save it:
openssl rand -hex 32Select Deploy to Cloudflare:
Enter these values when prompted:
Variable Value OPENAI_API_KEYThe OpenAI key used to retrieve session state OPENAI_EXECUTOR_API_KEYThe restricted executor key OPENAI_AGENT_IDThe agent ID from step 1 OPENAI_WEBHOOK_SECRETpending-webhook-registrationfor the first deploymentEXECUTOR_CLIENT_SECRETThe 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, whichwrangler.jsoncin the template sets to 30 seconds. -
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.createdagent.session.action_requiredagent.session.in_progressagent.session.idleagent.session.failed
Copy the signing secret that OpenAI returns. In Settings > Variables and Secrets for the Worker, replace
OPENAI_WEBHOOK_SECRETwith it, then select Deploy. If you deployed manually, run this command in theopenai/agents-apidirectory instead:npx wrangler secret put OPENAI_WEBHOOK_SECRETCheck the setup:
curl --fail-with-body "$WORKER_URL/health"The Worker is ready when the response contains
"configured": trueand"webhook_configured": true. -
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:
-
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 -
Generate a shared secret for the container cleanup endpoint, and save it:
openssl rand -hex 32 -
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 -
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."
}
]
}
]
}
]
}'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.
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.
-
The application creates or retrieves an OpenAI session and sends input through the Agents API.
-
When
EXECUTOR_PREWARM_ENABLEDistrue, as in the template, anagent.session.createdwebhook makes the Worker retrieve the session and start its container with the environment ID and remote URL of the session. -
An
agent.session.action_requiredwebhook 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. -
The Durable Object named for the session starts a container with those connection details and the executor key.
codex exec-serverconnects out to OpenAI. -
Each container start, environment connection, and
agent.session.in_progresswebhook resets a deadline ofEXECUTOR_KEEP_ALIVE_SECONDS. When the deadline passes, the Worker retrieves the session. If the session is still active, the Worker sets a new deadline. -
An
agent.session.idlewebhook makes the Worker snapshot the container and reset the deadline. When the deadline passes, the Worker stops the container and keeps the snapshot. -
New input sends another
agent.session.action_requiredwebhook. The Worker reuses a running container for the same environment ID, or starts the next environment from the saved snapshot.
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.
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.
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_SECRETstay in Worker secrets. The executor key goes into the container asCODEX_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-servercan 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
/webhookwithout an interactive Access login. The Worker checks the signature on each webhook, and the cleanup endpoint requiresEXECUTOR_CLIENT_SECRET. If you protect other routes with Cloudflare Access, use path-specific policies that leave/webhookreachable.
For more information about what a sandbox exposes to the code inside it, refer to Sandbox security.