1. System‑prompt as a contract, not pretty prose
In previous modules we looked at the ChatGPT App from an architectural angle: the widget, tools, and the MCP server. In this lecture we switch to the exact words we use to explain the model’s role: what it can do, what it must not do, and how to use tools. Essentially, we will design the system‑prompt as a contract between you and the model.
In a regular ChatGPT chat, you may be used to prompts like “Imagine you are a cheerful pirate assistant” or “Explain everything as if to a five‑year‑old.” That’s all about persona and style. In the context of a ChatGPT App, the term system‑prompt has a very different meaning.
Here, the system‑prompt is the first, hidden system‑role message system that tells the model:
- who it is within your App;
- what task it is focused on;
- what it does NOT do;
- how and when it should invoke your application’s tools.
In essence, it is a behaviour specification, very close to a formal contract. If agreed — the model tries to follow it. If not — it will act like a regular “general” ChatGPT, and your beautiful App will be left somewhere aside.
Importantly, for a ChatGPT App this system‑prompt:
- is bound to the application itself, not to a single specific dialogue;
- imposes constraints on both widget usage and tools calls;
- lives as long as the App session lives (while the user is interacting with your application).
To simplify, the deal is: you give the model tools and describe its role — what assistant it is, what it does, what it does not do, how it uses tools, and how it treats user data. In return, the model tries to behave that way.
2. Where the system‑prompt lives in the architecture
To avoid making it seem like some magical entity, it helps to see it on a diagram.
sequenceDiagram
participant User as User
participant ChatGPT as ChatGPT + model
participant App as Your ChatGPT App
participant MCP as MCP/Backend
Note over ChatGPT: At App initialization
the model is given the system‑prompt
User->>ChatGPT: "Pick a gift for a friend up to $50"
ChatGPT->>ChatGPT: Applies system‑prompt + tool descriptions
ChatGPT->>App: callTool recommend_gifts(...)
App->>MCP: HTTP /tools/recommend_gifts
MCP-->>App: List of gifts
App-->>ChatGPT: Tool result (JSON)
ChatGPT-->>User: Text + a pointer to the widget with cards
The system‑prompt is passed to the model when the App is initialised, at the moment ChatGPT decides to “attach” your application to the dialogue. After that, every decision ChatGPT makes — whether to call a tool, suggest a widget, or what to say when there’s no data — already passes through the lens of this contract.
From the Apps SDK perspective it is usually just a string sitting near the App configuration, for example:
// app/config/systemPrompt.ts
export const giftGeniusSystemPrompt = `
You are GiftGenius, a gift selection assistant...
`;
And then this string goes to where your application is wired up to ChatGPT. The technical “plumbing” may differ, but what matters for you is: the system‑prompt is a code artifact, just like a tool schema or a React component, and it needs to be designed and stored just as carefully.
Now that it’s clear where the system‑prompt lives in the architecture and how it gets to the model, the most important question is what exactly to put into it so it is a contract, not just a description of a “cute persona.”
3. App role and boundaries of responsibility
The first and main section of any decent system‑prompt is who you are and what you are responsible for.
The difference between “persona” and an “App contract” is simple: saying “you are a pirate and speak like a sailor” is about tone; saying “you are an interface to our gift catalogue, you select gifts and do not stray into other topics” is a contract.
For our training application GiftGenius, which selects gifts, the core of the role might look like this:
Role:
- You are GiftGenius, a gift selection assistant of our service.
- Your task is to help the user choose a suitable gift using only our catalog.
- You do not provide medical, legal, or financial advice.
Note the emphasis.
First, we define the domain narrowly: only gift selection within our service. This is needed so the model doesn’t start “explaining quantum physics” instead of opening your App, and so it doesn’t try to build strange workflows around other applications on your behalf.
Second, we explicitly fix what the App does not do. For example:
- does not give advice on topics unrelated to gifts and shopping;
- does not invent gifts that are not in the catalogue;
- does not make decisions for the user — only suggests options and explains the reasoning.
Such negative constraints often matter more than positive instructions: the model already “can do everything”; in the system‑prompt you are cutting off the excess.
For more complex environments where a single developer has multiple Apps (for example, “gift selection” and “tracking order delivery”), it is useful to state directly in the system‑prompt that in this App you handle only gift selection, you do not manage orders and logistics, and you do not launch other applications. This reduces the risk of the model getting confused about “whose job this is.”
4. When to invoke the App and when to answer on your own
The next critical block of the contract: rules for using tools.
If you don’t specify them, things usually go to one of two extremes:
- the model almost never calls your App because it’s easier and cheaper to answer “from its head”;
- or, conversely, it starts calling tools for any reason, even for the most theoretical questions.
In the App’s system‑prompt you need to clearly specify:
- in which cases you must use tools;
- in which cases you must answer on your own, without tools;
- what to do if the user explicitly asks to “not launch the application.”
A sample text fragment for GiftGenius:
Working with tools:
- Use the App tools when you need to fetch factual data from the catalog (list of gifts, prices, product types, delivery availability and discounts).
- Answer on your own if the question is theoretical and does not require catalog access (for example, "what gifts are commonly given for a housewarming").
- If the user explicitly asks to "not open the application" or "answer without a widget," respect that and do not call tools.
Several important things are happening here.
First, we tie invoking tools to the type of request: factual/catalogue data → tool; general theory → the model answers on its own.
Second, we explicitly state respect for the user’s intent: if a person writes “don’t launch anything, just explain,” the model should not ignore that signal.
Third, we thereby control the frequency of App usage. A good system‑prompt helps the model find a balance: the App is used when needed, but does not turn into an annoying pop‑up that jumps in all the time.
Later, in the next lecture on UX instructions, we will separately discuss how exactly the model should announce launching a widget and what to say after the scenario completes. Here we care about the decision rules: use the App or not.
5. Safe use of tools and handling user data
Now — about safety and common sense.
Your App’s tools vary:
- those that work with public data (gift catalogues, item availability, delivery terms);
- those that work with personal data and/or perform actions on behalf of the user (creating an order, charging funds, changing settings).
In the system‑prompt, you need to indicate how the model should treat these differences.
A typical set of rules:
Security and privacy:
- Do not perform actions that require user consent (purchase, subscription, changing personal data) without explicit confirmation in the chat.
- Do not pass more data to tools than is necessary for them to work (data minimization).
- If a request concerns sensitive data (health, finances, children), first clarify whether the user confirms sending this data to the application.
Here we address several goals at once.
First, we protect the user from unexpected activity: the model has no right to buy a gift or place an order on its own if you gave it such a tool. First — text confirmation, then — tool invocation.
Second, we reduce the risk of unnecessary data leakage: the model tends to “drag everything it sees” into tool arguments; you explicitly ask to limit to the minimally required fields.
Third, we highlight sensitive domains where even without a financial tool there may be legal/ethical risks.
A good practice is to annotate dangerous tools directly in their description (description) that they change state or perform payments, and to duplicate this at the system‑prompt level. This gives you a double barrier: both in the contract and in the specific tool’s description.
6. Format and style of the system‑prompt: write it like a specification
One of the most common mistakes is writing the system‑prompt like marketing copy: “You are an innovative, incredibly smart assistant that makes the world a better place…” It’s nice, but the model doesn’t care. It cares about:
- who I am;
- what to do;
- what not to do;
- how to use tools;
- how to treat data and other Apps.
Therefore it’s better to treat the system‑prompt as a specification:
- split it into logical blocks: “Role,” “Tasks,” “Boundaries,” “Working with tools,” “Security”;
- inside blocks, write short, unambiguous sentences;
- explicitly separate “do” and “don’t” (yes, lists are perfectly fine inside the prompt itself).
A fragment of a structured prompt for GiftGenius might look like this:
Role:
- You are GiftGenius, a gift selection assistant of our service.
Tasks:
- Help the user select gifts for the goal, the recipient’s interests, and the budget.
- Explain the pros and cons of each option in plain language.
Do not:
- Do not invent gifts that are not in the catalog.
- Do not promise service features that do not exist (for example, free delivery if it is paid in the catalog).
Keep the style neutral and “dry”: this is not sales copy but a contract. The fewer ambiguities, the more stable the behaviour.
Another important practice: versioning and storing the system‑prompt in the repository alongside the code. Prompts have versions too, and changes to them can break behaviour just as much as changes in your TypeScript logic. It’s much nicer in PR review to see a diff:
- Do not invent gifts that are not in the catalog.
+ Do not invent gifts that are not in the catalog, even if the user explicitly asks to "make something up".
than trying to remember that you “tweaked the wording a bit right in the interface.”
7. A complete example of a system‑prompt for our training App
Let’s put everything together and write a tidy system‑prompt for our training GiftGenius. We’ll split it into chunks to make it easier to read and edit.
First, describe the role and tasks:
Role:
- You are GiftGenius, a gift selection assistant of our service.
- You communicate with the user politely and professionally, without slang or jokes unless the user uses them first.
Tasks:
- Help select gifts by the given parameters (recipient profile, their interests, occasion, budget).
- Explain in simple, clear language why you are suggesting these specific options.
Now define boundaries and constraints:
Boundaries of responsibility:
- Work only with our gift catalog and its metadata.
- Do not invent gifts, promotions, or discounts that are not in the catalog or in tool responses.
- Do not provide medical, legal, or financial advice.
- Do not take responsibility for other applications or websites; if the user asks about them — say you cannot help.
Add rules for working with tools:
Working with tools:
- Use the `profile_to_segments` tool when you need to turn a free‑form recipient description into interest segments.
- Use the `recommend_gifts` tool when you need to find or filter gifts by user parameters (segments, budget, occasion, locale).
- Use the `get_gift` tool when the user needs details on a specific gift (description, type, price, delivery restrictions).
- Before invoking tools, try to clarify missing parameters (recipient’s age, budget, occasion) if the result would be useless without them.
- If the request is theoretical (for example, "how to generally choose gifts for a first wedding anniversary"), answer on your own without invoking tools.
Now — the block about safety and acting on behalf of the user:
Security:
- Do not place a purchase, subscription, or send a gift without explicit user confirmation in the chat.
- If a tool requires personal data (recipient’s e‑mail, delivery address, name), first explain to the user why it is needed and ask for confirmation.
- Do not pass more data to tools than necessary (for example, do not send the full message if age, interests, and budget are sufficient).
And overall tone/global rules:
General rules:
- If tools return an empty result, say so honestly and suggest relaxing constraints (change budget, gift type, category, or occasion).
- If the user asks to "not open the application" or "do without a widget," respect that and reply with text only, without calling tools.
- If the request is not related to gifts or shopping, answer like base ChatGPT and do not use GiftGenius tools.
As a result, this construction closely resembles the example from the additional materials on the topic: there are sections “Role,” “Tasks,” “Do/Don’t,” “Working with tools,” “Security,” “General rules.”
In Next.js code you can put this in a separate module:
// app/config/giftGeniusPrompt.ts
export const giftGeniusSystemPrompt = `
Role:
- You are GiftGenius, a gift selection assistant of our service.
...
General rules:
- If the request is not related to gifts, answer like base ChatGPT and do not use GiftGenius tools.
`;
And then use this constant in your App configuration (how exactly depends on the Apps SDK version, but the idea is the same: this text goes into the system role when the App dialogue is initialised).
8. Dynamic context in the system‑prompt
Sometimes the system‑prompt needs a bit of “dynamic seasoning”: current date, locale, user type (new/returning customer), subscription status, etc.
For example, if your gift catalogue and prices differ by region, you can pass the current region into the system‑prompt:
export function buildSystemPrompt(locale: string) {
return `
Role:
- You are GiftGenius, a gift selection assistant for the ${locale} region.
Boundaries:
- Use only the gifts and prices that are available in the ${locale} region.
...
`;
}
The Apps SDK can provide you with _meta["openai/locale"] at App initialisation, and you can generate the needed prompt variant from it. We will cover localisation in detail later, but it’s already helpful to see that the system‑prompt does not have to be strictly static.
The main thing is not to turn it into spaghetti conditions. If the logic becomes too complex, it’s better to split the App or push conditions down into tools (e.g., so the MCP server chooses the right data source by locale), and keep only high‑level rules in the system‑prompt.
9. How the system‑prompt relates to tools descriptions and UX instructions
This lecture focuses on the system‑prompt, but in a real App it does not exist in isolation. There are also tool descriptions (description, inputSchema) and follow‑up examples that you will define in later topics. Together they form a unified instruction system.
Managing tools invocation:
- the system‑prompt defines the general philosophy: “tools only for factual data,” “do not invent gifts,” “do not purchase without confirmation”;
- tools descriptions clarify what exactly recommend_gifts does, which parameters are needed, and when it should be invoked;
- follow‑up phrases set the dialogue style after a tool call: how to honestly say that nothing was found, how to suggest changing the query, how to summarise the results.
If these three layers are aligned, the model behaves predictably:
- calls the App when it is actually needed;
- does not hallucinate gifts/items outside the database;
- clearly explains to the user what happened (found / not found / more info needed).
If they are not — you get chaotic behaviour and long sessions of “magical prompt tuning,” which everyone gets tired of very quickly.
10. Common mistakes when working with the system‑prompt of a ChatGPT App
Error #1: writing a system‑prompt “for beauty,” not as a contract.
Developers often stick to generic phrases like “Help the user solve their tasks as best you can” and “Be friendly.” This does not make it clearer to the model when to invoke the App, where the boundaries of responsibility lie, whether it can invent data, and what to do when a tool errors. As a result, half the logic is smeared across code stacks and the author’s head instead of being embedded in an explicit contract.
Error #2: a role that is too broad (“help with everything”).
If in the role section you write “You are an assistant who helps with all user questions,” the model will happily do exactly that and not always remember your App. The App becomes an optional extra that is rarely used because the model thinks it can handle things itself. It’s better to state the niche explicitly: gift selection, working with the gift catalogue, help in a specific domain.
Error #3: no rules for when to call tools.
Formulations like “use tools as needed” are too vague. The model may either completely ignore tools or call them even where it could answer “from its head.” You need to clearly separate scenarios: factual data → tool, background explanations → the model’s own answer, explicit user refusal of the App → text only.
Error #4: trying to fix hallucinations with a single “don’t invent” phrase.
The phrase “don’t hallucinate” by itself helps little. It is important to explicitly describe what is forbidden to invent (items/gifts outside the catalogue, entries without IDs, non‑existent discounts) and what to do when the result is empty (honestly say nothing was found). Without that, the model will still try to “please” and generate fictional options. You need the full set: a global restriction in the system‑prompt, constraints in tools descriptions, and answer templates for “nothing found” cases.
Error #5: ignoring safety and user consent.
If the system‑prompt doesn’t state that purchase, booking, or changing personal data requires explicit confirmation, the model may, relying on “good intent,” call the tool on its own. From a UX perspective, this is a disaster. Always specify that any actions involving finances or the account are performed only after explicit consent in the chat.
Error #6: not accounting for other Apps and tools.
In a world where one account has multiple Apps and lots of tools, you cannot assume the model will “figure out by itself” which application to use. If the system‑prompt does not stipulate that this is specifically the App for gift selection and only for that, the model may unpredictably switch between different Apps or try to misuse tools.
Error #7: hot‑editing the system‑prompt without versioning and tests.
It’s tempting to hop into the config, change a line, and believe it “only got better.” In practice, any prompt tweak can break behaviour in other scenarios. If you do not store the system‑prompt in the repository, do not review diffs, and do not run a set of test queries (a golden prompt set — we will get to it in this module), you will be chasing regressions for weeks.
GO TO FULL VERSION