DEV Community

Sukhpinder Singh
Sukhpinder Singh

Posted on

OpenAI Responses API previous_response_id Instructions: Repeat Policy on Every Turn

OpenAI Responses API previous_response_id instructions have a counterintuitive boundary: the response ID carries conversation state forward, but it does not carry the prior top-level instructions. If my application uses that field for output format, tool rules, or safety constraints, turn two can quietly run without the policy I expected.

I prefer to make that boundary visible in request construction. A small guard can require instructions on every policy-bound turn, while still allowing the application to replace them deliberately.

Why OpenAI Responses API previous_response_id instructions disappear

The Responses API supports several state strategies. With previous_response_id, a new response can continue from an earlier one without resending the full conversation. OpenAI's conversation state guide shows that chaining pattern directly.

That convenience does not make every request option persistent. The create response reference says that earlier instructions are not carried into a request that uses previous_response_id. The new request can supply the same instructions, different instructions, or none.

This is useful when I want to change behavior mid-thread. It is risky when the omission is accidental.

Consider an application that expects every answer to follow a JSON contract:

const string Policy =
    "Answer in JSON with keys summary and risks.";

var first = new
{
    model = "gpt-5.6",
    instructions = Policy,
    input = "Review the deployment plan.",
    store = true
};
Enter fullscreen mode Exit fullscreen mode

A continuation that sends only the new input and the earlier response ID has conversation context, but not that top-level policy. I therefore build the second turn with the policy beside the continuation ID:

var second = new
{
    model = "gpt-5.6",
    instructions = Policy,
    input = "Now focus on rollback.",
    store = true,
    previous_response_id = firstResponseId
};
Enter fullscreen mode Exit fullscreen mode

The distinction is simple: response lineage and request instructions solve different problems. I do not treat one as an implied copy of the other.

Make the continuation contract explicit

I encode the rule in one request builder instead of relying on every call site to remember it. The builder accepts the model, input, instructions, and one optional state reference:

internal sealed record ResponsesTurn(
    string Model,
    string Input,
    string Instructions,
    string? PreviousResponseId = null,
    string? ConversationId = null);
Enter fullscreen mode Exit fullscreen mode

The guard rejects blank instructions before any transport code runs:

ArgumentException.ThrowIfNullOrWhiteSpace(turn.Model);
ArgumentException.ThrowIfNullOrWhiteSpace(turn.Input);
ArgumentException.ThrowIfNullOrWhiteSpace(turn.Instructions);
Enter fullscreen mode Exit fullscreen mode

This is an application invariant, not an API requirement. The API's instructions field is optional. My builder makes it mandatory because this particular application says every turn must carry policy.

I also reject a payload that combines previous_response_id with conversation:

if (turn.PreviousResponseId is not null &&
    turn.ConversationId is not null)
{
    throw new InvalidOperationException(
        "previous_response_id and conversation cannot be sent together.");
}
Enter fullscreen mode Exit fullscreen mode

That mirrors the documented request contract and keeps the state strategy unambiguous. If I decide to move from response chaining to a durable Conversation object, I make that a deliberate code change rather than emitting both fields and waiting for an HTTP 400.

Verify the chained payload offline

The merged sample on main is a dependency-free .NET 10 executable. It builds three fixed payloads: a first turn, a continuation that repeats the policy, and a continuation that intentionally replaces it.

Its verifier checks seven behaviors. It confirms that the first request carries instructions and store: true; the chained request carries both the policy and its fixture response ID; replacement instructions survive serialization; blank instructions and IDs fail locally; the two state mechanisms cannot be combined; and identical inputs produce byte-identical JSON.

The merged pull request records the exact validation commands and results. Restore, formatting, Release build, package inventory, and vulnerability audit pass. Five repeated verifier runs produce identical output. No account, API key, paid request, or model call is needed.

I like this test because it validates the part my code controls. A model call would add latency and output variability without proving that every future call site constructs the payload correctly. The offline fixture makes omission a deterministic build failure.

Limits and when not to use this guard

This sample is not an OpenAI SDK replacement. It writes a narrow JSON shape with string input, sets store: true, and stops before HTTP. A production integration should use a current supported SDK or a complete HTTP client, handle API errors, and test its actual serialization boundary.

The guard also does not fit every state strategy. With store: false, I need to replay the relevant returned items instead of chaining a stored response. A durable Conversation object has its own lifecycle and cannot be combined with previous_response_id. Stored response objects also have retention and token-billing implications described in the conversation state guide, so state selection deserves an explicit product decision.

Finally, model instructions are not authorization. I still enforce permissions, tenant boundaries, validation, and destructive-action checks in application code. Repeating policy keeps the model request consistent; it does not turn prose into a security boundary.

What invariant would you add before a chained request leaves your code?

Happy coding!

Top comments (0)