Short answer: the Responses API remembers earlier turns only if you ask it to, with previous_response_id or a conversation. Otherwise each call sees only the input it carries. And when it does carry state, it carries the whole conversation: every earlier input token is billed again on each call, and nothing is ever let go.
This guide covers how the Responses API keeps state, what it does with reasoning and tool calls, what changed with the end of the Assistants API, and how to replace chaining with a memory: each call sends only what is new, and the request that comes back holds what the model should see.
Does the Responses API remember earlier turns?
Not by default. There are three ways to give the next call the earlier turns, all from OpenAI's conversation state guide:
- Send them yourself. Append each response's
outputitems to yourinputarray and send the whole array next time. - Chain with
previous_response_id. Name the last response and send only the new input. OpenAI looks up the stored chain. - Attach a
conversation. Items from the conversation are prepended to the input, and the response's input and output items are added to the conversation when it completes (API reference).
Chaining, in TypeScript:
import OpenAI from 'openai';
const openai = new OpenAI();
const first = await openai.responses.create({ model: 'gpt-5', input: 'Tell me a joke.' });
const second = await openai.responses.create({
model: 'gpt-5',
previous_response_id: first.id,
input: [{ role: 'user', content: 'Explain why it is funny.' }],
});
console.log(second.output_text);The same in Python, and with a conversation instead of a chain:
from openai import OpenAI
client = OpenAI()
first = client.responses.create(model="gpt-5", input="Tell me a joke.")
second = client.responses.create(
model="gpt-5",
previous_response_id=first.id,
input=[{"role": "user", "content": "Explain why it is funny."}],
)
conversation = client.conversations.create()
reply = client.responses.create(
model="gpt-5",
conversation=conversation.id,
input=[{"role": "user", "content": "Tell me a joke."}],
)And over HTTP:
curl https://api.openai.com/v1/responses \
-H "Authorization: Bearer $OPENAI_API_KEY" -H "Content-Type: application/json" \
-d '{"model":"gpt-5","previous_response_id":"resp_123","input":[{"role":"user","content":"Explain why it is funny."}]}'previous_response_id and conversation cannot be used together in one request (API reference).
Ways to carry state, side by side
Manual input array | previous_response_id | Conversations API | Threadless AI | |
|---|---|---|---|---|
| Who stores the history | You | OpenAI, as stored responses | OpenAI, as conversation items | Us, per memory |
| How long it is kept | As long as you keep it | 30 days by default | Until deleted | Until you delete the memory |
| Input billed per call | The whole array | All earlier input tokens in the chain | Every item in the conversation | What the returned request holds, which stops growing |
| What is let go | Whatever you cut | Nothing | Nothing | Older messages, word for word, once their facts are noted; they come back when the conversation turns to them |
Works with store: false / ZDR | Yes | Only by replaying items instead | No | Yes: store is dropped from the request |
Retention and billing are from OpenAI's conversation state guide and data controls. With store: false, OpenAI's programmatic tool calling guide says to replay the complete item sequence instead of continuing from previous_response_id.
Are instructions carried over with previous_response_id?
No. The reference for instructions says: "When using along with previous_response_id, the instructions from a previous response will not be carried over to the next response" (API reference). Send instructions on every call. It also means you can change them mid-conversation.
Reasoning items and tool calls across turns
- Reasoning items are encrypted by default. "The Responses API returns encrypted reasoning items by default." For stateless requests, OpenAI says to "preserve every item in the response's
outputarray" when you replay it (conversation state). include: ["reasoning.encrypted_content"]returns an encrypted version of the reasoning tokens in reasoning items, so you can carry them forward without storage (API reference).- Tool results go back as
function_call_outputitems with thecall_idof the call they answer. Keep eachfunction_calland its output together: the model expects both.
With Threadless AI, send the output items exactly as returned. Reasoning items are stored as sent and come back in the same format.
What happens after 30 days?
Stored responses are kept "for at least 30 days" by default (API reference), so a conversation that sits idle beyond that can lose the chain it depends on. Do not build long-lived conversations on previous_response_id alone. Conversation items have no 30-day TTL (conversation state), and a memory keeps its history until you delete it.
Migrating from the Assistants API
The Assistants API is gone. OpenAI announced its deprecation on 26 August 2025 and removed it on 26 August 2026; the replacement is the Responses API and the Conversations API (Deprecations). OpenAI's Assistants migration guide maps the concepts:
| Assistants API | Now |
|---|---|
| Assistants | Prompts (configuration: model, tools, instructions) |
| Threads | Conversations |
| Runs | Responses |
| Run steps | Items |
Points to know before you move:
- Threads can no longer be read from the API. The migration guide says the call that retrieves thread messages no longer works; rebuild history from messages your application stored.
- Prompt objects are going too. OpenAI's deprecations page schedules the
v1/promptsAPI and reusable prompt objects to shut down on 30 November 2026, and says to move prompt content into application code. Put your old assistant's instructions ininstructions. - Conversations take up to 20 items per call when created or appended to (Conversations reference). Import long histories in batches.
- Threads were a log, not a memory. Moving a thread into a conversation keeps the same property: every item is sent on every call.
If you are rebuilding anyway, you can skip the conversation: create a memory, send the stored messages as the first call's new items, and continue from there with the loop below.
Add long-term memory: install and create a memory
npm install @threadlessai/sdk # or: pip install threadlessai
export THREADLESS_API_KEY=tl_test_...Create one memory per conversation and store its id next to yours.
import { Threadless } from '@threadlessai/sdk';
const memory = new Threadless();
const { id: memoryId } = await memory.memories.create({ metadata: { conversation: 'c_123' } });The loop, in TypeScript
import OpenAI from 'openai';
import { isNoteCall, NOTE_RESULT, Threadless } from '@threadlessai/sdk';
const openai = new OpenAI();
const memory = new Threadless();
const tools = [memory.responses.noteTool, ...yourTools];
export async function send(memoryId: string, text: string) {
let newItems: Array<Record<string, unknown>> = [{ role: 'user', content: text }];
for (;;) {
const request = await memory.responses.decorate(memoryId, newItems, { model: 'gpt-5', tools });
const reply = await openai.responses.create(
request as OpenAI.Responses.ResponseCreateParamsNonStreaming,
);
memory.responses.report(memoryId, reply);
const calls = reply.output.filter((item) => item.type === 'function_call');
if (calls.length === 0) return reply.output_text;
newItems = reply.output.map((item) => ({ ...item }));
for (const call of calls) {
const output = isNoteCall(call.name) ? NOTE_RESULT : await runTool(call);
newItems.push({ type: 'function_call_output', call_id: call.call_id, output });
}
}
}The loop, in Python
from openai import OpenAI
from threadlessai import NOTE_RESULT, Threadless, is_note_call
openai = OpenAI()
memory = Threadless()
tools = [memory.responses.note_tool, *your_tools]
def send(memory_id: str, text: str):
new_items = [{"role": "user", "content": text}]
while True:
request = memory.responses.decorate(memory_id, new_items, {"model": "gpt-5", "tools": tools})
reply = openai.responses.create(**request)
memory.responses.report(memory_id, reply)
calls = [item for item in reply.output if item.type == "function_call"]
if not calls:
return reply.output_text
new_items = [item.model_dump(exclude_none=True) for item in reply.output]
for call in calls:
output = NOTE_RESULT if is_note_call(call.name) else run_tool(call)
new_items.append({"type": "function_call_output", "call_id": call.call_id, "output": output})What changes from a stateful Responses agent
- Drop
previous_response_idandstore. If you pass them, they are removed from the request, because we hold the history. Everything else in the request (model,tools,reasoning,instructions,tool_choice) passes through unchanged. - Send only what is new. The first call: your developer message and the user's message. A new turn: the model's last output and the user's message. Inside a tool loop: the output items and your
function_call_outputitems. Output you already reported is recognised and not stored twice. - Call
decoratebefore every model call, including each call inside a tool loop. - Report every reply, exactly as returned,
usageincluded. The usage lets us correct our size estimate for your model.
What the request holds
The input that comes back has your developer message first, then a facts block of what was learned from older messages, then the latest messages word for word, with a recall block before a user message that touches something old. When older messages are about to stop being sent word for word, the request ends with a note ask; the model answers it with one call to the note tool, and your loop answers that call with noted.
Token use stays bounded: the request stops growing with the conversation (How it works).
Streaming
If you stream, report the final response once the stream ends: the response of the response.completed event. See Add memory to a streaming chatbot.
Questions people ask
Is the Responses API stateful?
It can be. Responses are stored by default and you can chain to them or attach a conversation, but a call with neither sees only its own input. The state it keeps is the full transcript, billed again on every call.
Does store: false break previous_response_id?
Over HTTP, yes in practice: with store: false OpenAI's guidance is to replay the full item sequence rather than continue from previous_response_id. Zero Data Retention forces store to false.
How long does OpenAI keep responses?
Thirty days by default for stored responses; store: false turns storage off. Conversations and their items are kept until you delete them.
Can I use previous_response_id with my own memory?
Not with Threadless AI: we drop previous_response_id and store from the decorated request, because the request already holds what the model should see. Chaining on top would make OpenAI prepend the whole stored chain again.
Does it work with streaming?
Yes. Stream the request decorate returns as you normally would, and report the final response from the response.completed event.
What replaced Assistants API threads?
Conversations. OpenAI removed the Assistants API on 26 August 2026 and maps threads to conversations, runs to responses and run steps to items.