Assistants to Responses skill for Codex
Get off the shut-down Assistants API safely. Codex inventories every beta.assistants and threads call, moves assistant instructions and tools into versioned code, and rewrites runs and polling as responses.create with a capped function_call loop. Legacy threads are backfilled into Conversations from your own stored history after you confirm the plan, streaming handlers and citation annotations are remapped, and a replay set proves the new path behaves like the old one.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- assistants-to-responses-migration Installs to ~/.agents/skills/assistants-to-responses-migration/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install assistants-to-responses-migration The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project assistants-to-responses-migration Installs to .agents/skills/assistants-to-responses-migration/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/assistants-to-responses-migration.zipunzip -o assistants-to-responses-migration.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) assistants-to-responses-migration PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $assistants-to-responses-migration, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Migrates OpenAI Assistants API applications to the Responses API and Conversations - assistants to instructions and tools in code or prompt objects, threads to conversations with backfilled items, runs and polling to responses.create, requires_action tool loops to function_call and function_call_output items, file_search and code_interpreter tool configuration, streaming event mapping and citation annotations. Use when code still calls beta.assistants, beta.threads or runs (the Assistants API was shut down on 26 August 2026), when an assistant-based app is failing, or when planning the data and cutover for a migration.
Use it when
- Code still calls beta.assistants, beta.threads or threads.runs
- An assistant-based chatbot broke after the August 2026 shutdown
- Planning data backfill and cutover from threads to Conversations
- Converting Assistants streaming handlers or file_search citations
Use something else when
- You are migrating from Chat Completions, not Assistants: use openai-responses-api
- You want a managed agent framework rather than raw API calls: use openai-agents-sdk
How to check the result
A search for Assistants call sites returns nothing, and a replay of real conversations through the new path matches or beats the old answers with tool calls and citations intact.
The mistake it prevents
Assuming thread history can still be fetched from the API after shutdown, or sending instructions only on the first turn when Responses applies them per request.
The OpenAI Assistants API was deprecated in August 2025 and shut down on 26 August 2026.
Any code path still calling beta.assistants, beta.threads or threads.runs is broken
or about to be. Migration is done when no Assistants calls remain, every existing user
conversation either continues in a Conversation or is explicitly archived, tool loops run
in your code with caps, and behaviour on a replay set matches the old app.
Concept map
| Assistants API | Responses API | What changes for you |
|---|---|---|
| Assistant (model, instructions, tools) | instructions + tools in code, or a Prompt object (prompt={"id": ...}) | Config moves into version control (or a dashboard prompt) |
| Thread | Conversation (conversations.create) | Holds items: messages, tool calls, tool outputs |
| Message | Item (message with input_text / output_text content) | Typed content parts |
| Run + polling | responses.create (synchronous, streamed, or background=True) | No polling loop for normal calls |
| Run step | Output items (function_call, file_search_call, code_interpreter_call, message) | Inspect response.output by type |
requires_action / submit_tool_outputs | function_call items answered by function_call_output items in the next request | Your code owns the loop |
tool_resources on assistant/thread | vector_store_ids on the file_search tool; container on code_interpreter | Per request |
additional_instructions | instructions on that request | Not inherited across turns |
response_format | text={"format": ...} | Structured outputs syntax |
truncation_strategy | truncation="auto" or compaction | Context management |
Full parameter, event and annotation mapping: read references/mapping.md when converting a specific call site or streaming handler.
Inventory before changing code
- Read
AGENTS.md, then find every call site:rg -n "beta\.(assistants|threads)|threads\.runs|submit_tool_outputs|create_and_poll|assistant_id|thread_id" . - List each assistant the code references (ids in env vars or the DB) with model, instructions, tools, vector stores and files. The dashboard may still show them for reference even though the API is gone; copy the instructions and tool schemas into the repo now.
- Find where thread ids are stored (users table, sessions). That is your mapping table.
- Find your own copy of message history. Since shutdown, thread messages can no longer be listed through the API, so history exists only if your app stored it (or an export was taken before shutdown). Tell the user plainly if it does not exist.
- Vector stores and uploaded files are separate resources used by Responses
file_search; confirm they still list (client.vector_stores.list()) and reuse them.
Decisions
| Question | Default | Alternative |
|---|---|---|
| Where do instructions and tools live? | In code, versioned in git, one module per former assistant | Dashboard Prompt objects when non-engineers edit prompts; check OpenAI’s deprecation notes for reusable prompts before depending on them |
| How is state kept? | Conversations (conversation=conv_id) for user-visible chats | previous_response_id chaining for short flows; full client-side history with store=False for zero-retention requirements |
| Old history | Backfill into a new Conversation per thread | Archive read-only in your DB and start fresh conversations |
| Long-running runs | Synchronous or streamed responses.create | background=True and poll responses.retrieve(id) for tasks over a few minutes |
Code: before and after
# BEFORE (no longer works)
run = client.beta.threads.runs.create_and_poll(thread_id=thread_id, assistant_id=ASSISTANT_ID)
if run.status == "requires_action":
outputs = [{"tool_call_id": tc.id, "output": run_tool(tc)}
for tc in run.required_action.submit_tool_outputs.tool_calls]
client.beta.threads.runs.submit_tool_outputs_and_poll(
thread_id=thread_id, run_id=run.id, tool_outputs=outputs)
# AFTER
import json
from openai import OpenAI
client = OpenAI()
SUPPORT = { # was assistant asst_...
"model": "gpt-5-mini", # pick and evaluate; do not assume the old model
"instructions": open("prompts/support.md").read(),
"tools": [
{"type": "file_search", "vector_store_ids": ["vs_abc123"]},
{"type": "function", "name": "get_order", "description": "Look up an order by id.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"], "additionalProperties": False},
"strict": True},
],
}
def reply(conversation_id: str, user_text: str) -> str:
resp = client.responses.create(**SUPPORT, conversation=conversation_id,
input=[{"role": "user", "content": user_text}])
for _ in range(6): # cap tool rounds
calls = [o for o in resp.output if o.type == "function_call"]
if not calls:
return resp.output_text
outputs = [{"type": "function_call_output", "call_id": c.call_id,
"output": json.dumps(run_tool(c.name, json.loads(c.arguments)))}
for c in calls]
resp = client.responses.create(**SUPPORT, conversation=conversation_id, input=outputs)
raise RuntimeError("tool loop did not finish")
instructions apply to one request only; send them on every call (the module dict does
this). Tool calls and outputs are appended to the conversation automatically when you pass
conversation.
Backfilling threads into conversations
def to_items(messages): # from your own stored history, oldest first
items = []
for m in messages:
part = "input_text" if m.role == "user" else "output_text"
content = [{"type": part, "text": m.text}]
if m.role == "user" and m.image_url:
content.append({"type": "input_image", "image_url": m.image_url, "detail": "auto"})
items.append({"type": "message", "role": m.role, "content": content})
return items
conv = client.conversations.create(items=to_items(history[-40:]),
metadata={"legacy_thread_id": thread_id, "user_id": uid})
db.update_user_session(uid, conversation_id=conv.id)
- Backfill only recent turns (or a summary plus recent turns); full histories cost tokens on every future request.
- Run the backfill as an idempotent job keyed on the legacy thread id; it can be resumed.
- Writing to production data and creating conversations for every user is a bulk operation: show the plan and counts to the user and get confirmation before running it.
Cutover checklist
- Inventory call sites, assistants, vector stores, thread id storage, history source
- Copy instructions and tool schemas into the repo
- Build the Responses wrapper with tool loop, caps and logging
- Map streaming handlers to Responses events (references/mapping.md)
- Render
file_citation/url_citationannotations fromoutput_textparts - Backfill or archive old threads (after user confirmation); store conversation ids
- Replay 30-100 real conversations through old transcripts vs new path and compare
- Remove all
beta.assistants/beta.threadscode and dead env vars - Deploy behind a flag; confirm with the user before the production rollout
Verification
rgfor Assistants call sites returns nothing.- Replay set: answers are judged equivalent or better (use llm-evals graders), tool calls hit the same functions with valid arguments, citations render.
- Token and cost per conversation are logged; compare against the old app’s usage export.
- A migrated user opens an old chat and the next answer uses the backfilled context.
Gotchas
previous_response_idandconversationare alternatives; using both errors or duplicates context.- Responses default to
store=True. If the old app relied on zero-retention settings, setstore=Falseand replay context yourself (reasoning items needinclude=["reasoning.encrypted_content"]). - Function call ids: answer with
call_id, not the itemid. - Code interpreter files are now container files; download them through the containers API before the container expires, not through the old file ids.
- Model behaviour changes with the model, not the API. Evaluate the model choice separately from the plumbing migration.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Assistants to Responses"
short_description: "Get off the shut-down Assistants API safely"
default_prompt: "$assistants-to-responses-migration Our app still uses the OpenAI Assistants API and it stopped working, migrate it to Responses" references/mapping.md
# Assistants to Responses: detailed mapping
Use this when converting a specific call site. Confirm exact fields against the current
OpenAI API reference if a call fails; the SDK's type hints are the fastest check.
## Calls
| Assistants call | Responses replacement |
|---|---|
| `beta.assistants.create/update` | A config module in code (`model`, `instructions`, `tools`) or a dashboard Prompt |
| `beta.assistants.retrieve` | Import the config module |
| `beta.threads.create(messages=..., metadata=...)` | `conversations.create(items=..., metadata=...)` |
| `beta.threads.retrieve/update/delete` | `conversations.retrieve/update/delete` |
| `beta.threads.messages.create` | Pass the message as `input` on the next `responses.create`, or `conversations.items.create` to append without generating |
| `beta.threads.messages.list` | `conversations.items.list(conversation_id)` for new conversations; your own DB for legacy threads |
| `beta.threads.runs.create` / `create_and_poll` | `responses.create(..., conversation=conv_id)` |
| `beta.threads.runs.stream` | `responses.create(..., stream=True)` or `responses.stream(...)` |
| `beta.threads.create_and_run` | `conversations.create` then `responses.create` |
| `runs.submit_tool_outputs(_and_poll)` | Next `responses.create` with `function_call_output` items as `input` |
| `runs.cancel` | Abort the HTTP request; for `background=True`, `responses.cancel(response_id)` |
| `runs.steps.list` | Iterate `response.output` items |
## Run parameters
| Run / assistant field | Responses field |
|---|---|
| `assistant_id` | Spread your config (`model`, `instructions`, `tools`) or `prompt={"id": ..., "version": ...}` |
| `instructions` (override) | `instructions` |
| `additional_instructions` | Append to `instructions` for this request |
| `additional_messages` | Extra items at the start of `input` |
| `tools` | `tools` (same function schema, flattened: `name`, `description`, `parameters` at top level, plus `"strict": true`) |
| `tool_choice` | `tool_choice` |
| `parallel_tool_calls` | `parallel_tool_calls` |
| `temperature`, `top_p` | Same names, where the model supports them (reasoning models ignore or reject them) |
| `max_completion_tokens` | `max_output_tokens` |
| `max_prompt_tokens`, `truncation_strategy` | `truncation="auto"`; compaction for long conversations |
| `response_format` (json_schema) | `text={"format": {"type": "json_schema", "name": ..., "schema": ..., "strict": True}}` |
| `metadata` | `metadata` |
| `tool_resources.file_search.vector_store_ids` | `{"type": "file_search", "vector_store_ids": [...]}` in `tools` |
| `tool_resources.code_interpreter.file_ids` | `{"type": "code_interpreter", "container": {"type": "auto", "file_ids": [...]}}` |
## Function tool definition
```json
// Assistants (nested)
{"type": "function", "function": {"name": "get_order", "description": "...", "parameters": {...}}}
// Responses (flat)
{"type": "function", "name": "get_order", "description": "...", "parameters": {...}, "strict": true}
```
With `strict: true`, every property must be listed in `required` and
`additionalProperties` must be `false`; model optional fields as nullable types.
## Run status to Responses handling
| Run status | Responses equivalent |
|---|---|
| `queued`, `in_progress` | Request in flight; with `background=True`, `status` on `responses.retrieve` |
| `requires_action` | `response.output` contains `function_call` items |
| `completed` | `status == "completed"` with no pending function calls |
| `incomplete` | `status == "incomplete"`; check `incomplete_details.reason` (e.g. `max_output_tokens`) |
| `failed` | Exception from the SDK, or `status == "failed"` with `error` |
| `cancelled`, `expired` | Aborted request; background responses can be cancelled |
## Streaming events
| Assistants event | Responses event |
|---|---|
| `thread.run.created` | `response.created` |
| `thread.message.delta` | `response.output_text.delta` (`event.delta` is the text) |
| `thread.message.completed` | `response.output_text.done` / `response.output_item.done` |
| `thread.run.step.delta` (tool call args) | `response.function_call_arguments.delta` / `.done` |
| `thread.run.requires_action` | `response.output_item.done` with a `function_call` item, then `response.completed` |
| `thread.run.completed` | `response.completed` (carries `usage`) |
| `thread.run.failed` | `response.failed` / `error` |
Branch on `event.type`; ignore event types you do not handle so new ones do not break
the parser.
## Annotations and files
- File search citations arrive as `annotations` on `output_text` content parts with type
`file_citation` (`file_id`, `filename`, `index`). Map `file_id` to your document titles.
- Web search citations use `url_citation` (`url`, `title`, start and end indexes).
- Code interpreter outputs are container files, cited with `container_file_citation`.
Download them via the containers file-content endpoint while the container is alive.
- To get file search results themselves (not only citations), request
`include=["file_search_call.results"]`.
## Usage and cost
Responses report `usage.input_tokens`, `usage.output_tokens`,
`usage.input_tokens_details.cached_tokens` and `usage.output_tokens_details.reasoning_tokens`.
Conversations replay their items as input on every turn, so long conversations grow
input cost; cap backfilled history and use truncation or compaction.
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.