system_instruction in the Gemini API: A Separate Field From contents
8 min read · updated August 11, 2026
In the chat-completions shape, the system prompt is a message with role: "system" at the head of the array. Gemini does not have that role. The system prompt is a field of the request that sits beside contents, at the same level as generationConfig and tools, and posting it as a message fails in a way that produces no error at all.
Where the field actually sits
Here is a complete, valid generateContent body with a system instruction, in the shape Google’s generateContent API reference documents:
POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
x-goog-api-key: $GEMINI_API_KEY
content-type: application/json
{
"system_instruction": {
"parts": [
{ "text": "You are a support agent for an accounting product. Never guess at tax rates; say you do not know." }
]
},
"contents": [
{
"role": "user",
"parts": [{ "text": "What VAT rate applies to my invoice?" }]
}
],
"generationConfig": {
"temperature": 0.2,
"maxOutputTokens": 512
}
}Three structural facts are visible in that body and all three matter. First, system_instruction is a sibling of contents, not an element of it. Second, it is a Content object—the same type as an entry in contents—so it has a parts array, not a bare string. Third, it has no role. Supplying one is accepted and ignored; the field’s position is what gives it its meaning.
There is exactly one of it. Unlike a chat array, where nothing stops you interleaving several system messages, system_instruction is a single object per request. Multiple instructions go in as multiple parts of that one object, or concatenated into one part.
What happens if you put it in contents
The failure mode is the reason this page exists. contents entries accept two roles: user and model. There is no system role in the Gemini API. Send one and you get an error about an invalid role—which is the good case, because it is loud.
The bad case is the workaround people reach for next: putting the system text into a user turn at the front of the array. That is a valid request. It returns 200. The model reads the text, and for simple instructions it will often appear to work, which is what makes it dangerous. What you have actually built is a conversation whose first user turn contains your policy, and it degrades in three specific ways:
- It competes with later user turns instead of framing them. A genuine
system_instructionis handled by the model as standing configuration for the whole exchange. Text in the first user turn is one message among several, and a long conversation dilutes it—the classic report of “the model forgot its persona after twenty turns” almost always traces to this. - It is inside the attack surface. Instructions in a user turn look, to the model, exactly like instructions from the user. Content in a later turn that says “ignore the earlier message” has a plausible claim to be doing something reasonable.
- It breaks the alternation. Gemini expects
contentsto alternate user and model turns. A prepended pseudo-system user turn followed by the real user turn is twouserentries in a row, which the API tolerates by merging but which quietly changes what your conversation history looks like when you resend it.
systemInstruction or system_instruction
Both appear in Google’s own examples and both work. The REST API accepts either the snake_case protobuf field name (system_instruction) or its lowerCamelCase JSON name (systemInstruction), which is standard behaviour for Google APIs generated from protobuf definitions. Responses come back in lowerCamelCase.
The SDKs pick one. The Google Gen AI Python SDK takes it as system_instruction inside the config object; the JavaScript SDK uses systemInstruction. A plain string is accepted there as shorthand and expanded into the parts shape for you:
from google import genai
from google.genai import types
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="What VAT rate applies to my invoice?",
config=types.GenerateContentConfig(
system_instruction="You are a support agent for an accounting product. "
"Never guess at tax rates; say you do not know.",
temperature=0.2,
max_output_tokens=512,
),
)
print(response.text)Note where it lives in the SDK: inside the config, next to sampling settings, not inside contents. The library is reflecting the wire shape faithfully.
Four consequences of it being separate
- It is billed as input like anything else. A separate field is not a free field. The system instruction is tokenised and charged on every request, and it is included in the total that countTokens returns when you pass it.
- It can be cached. Because it is a stable prefix by construction, a long system instruction is exactly the kind of content explicit context caching exists for, and a cached content object can carry its own
system_instruction. - It does not survive a model swap unchanged. Nothing in the API validates that your instruction is appropriate for the model. Reasoning-oriented models treat lengthy step-by-step instructions differently from non-reasoning ones.
- It does not override safety settings. An instruction telling the model to answer anything does not raise a block threshold. Those are set in safetySettings, which is another separate top-level field, and the two are evaluated independently.
What belongs in the field, and what does not
The field is a single stable slot evaluated on every request. That shape suggests its own contents: things that are true for every turn of every conversation, and nothing that varies between them.
Belongs in it:
- Role and scope. What the assistant is for and what it declines to do. “You answer questions about this product’s billing behaviour. You do not give tax advice.”
- Output contracts that are not enforceable elsewhere. Tone, length conventions, whether to use markdown, what to do when the answer is not known. Note the qualifier: if the contract is a JSON shape, it belongs in responseSchema, where it is enforced at decoding rather than requested in prose.
- Stable domain facts. The product’s vocabulary, the names of the tiers, the fact that the fiscal year starts in April. Things that are as true on the twentieth turn as the first.
- Behaviour around tools. When to call, when to ask first, what to do with an empty result. The declarations say what the tools are; this says how to conduct yourself with them.
Does not belong in it:
- Per-turn retrieved context. Documents fetched for this question go in
contents. Putting them in the system instruction changes the stable prefix on every request, which is precisely what destroys a cache hit and inflates the fixed cost of every call. - Anything secret. The system instruction is text the model can be induced to repeat. Treat everything in it as potentially visible to the user, and keep credentials, internal policy documents and unreleased information out of it.
- Safety policy you need enforced. An instruction not to produce a category of content is conditioning, not a filter. The filter is safetySettings, and the two are evaluated independently.
The unifying test is whether the text would be identical on every request for the lifetime of the deployment. If it would, the separate field is exactly the right home for it, and the API’s decision to make it a field rather than a message is a decision in your favour.
Porting a system prompt from another API
The mechanical translation is small and worth writing down once, because most porting bugs are one of these three:
- Take every message with a system-ish role from the source request and concatenate them, in order, into one string. Gemini has one slot; multiple system messages have to be merged rather than repeated.
- Wrap it as
{"system_instruction": {"parts": [{"text": "..."}]}}at the top level of the body. Do not add arole. - Re-map the remaining messages:
assistantbecomesmodel,userstaysuser, and each message’s string content becomes apartsarray. Verify the result alternates.
The last step is where tool results go wrong, because a tool result in Gemini is a functionResponse part inside a user turn rather than a role of its own—see how function responses are threaded through a conversation.