Prompts through the API¶
The concept behind agents is explained in Agents.
On this page you store a prompt template through the API, add versions and use the prompt in a request. A stored prompt lets you reuse the same instructions across calls and update them without deploying your application. A request selects a fixed version or the latest, and the audit trail records the version used.
Your key needs the prompts capability. You do the same with a mouse in
Prompts in the console.
Create a prompt¶
curl -s -X POST "$CORE_URL/prompts" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "triage",
"template": "Classify the ticket below as billing, technical or other.\n\n{{ ticket }}",
"input_schema": {"properties": {"ticket": {"type": "string"}}, "required": ["ticket"]},
"role": "developer"}'
A successful request returns 201 and creates the prompt with version 1.
| Field | What it does |
|---|---|
name |
unique in your organisation; what a request references |
template |
the text, with {{ name }} for a variable and {{ $name }} for an install value |
input_schema |
every variable the template uses, declared as a JSON Schema object — and nothing else: one entry in properties per variable, and the names a call must fill in under required (below) |
install_values |
optional: a value for every {{ $name }}, such as {"company": {"type": "string", "value": "Acme"}} (below) |
role |
developer (the default) or delegate: the authority the rendered text carries |
metadata |
an opaque object of your own, at most 16 KiB serialised; Core stores and returns it and never reads it |
Templates support placeholder substitution only. Conditionals, filters and includes are not supported.
The input schema¶
input_schema is a JSON Schema object, so any tool that reads JSON Schema can build a form or a
call from it. Each property is one variable:
| Key | What it says |
|---|---|
type |
string, number, integer or boolean: what a call must send for it |
description |
optional: what the variable holds, for whoever calls the prompt |
enum |
optional: the only values allowed, each of the variable's own type |
default or x-builtin |
optional: what Core fills in when a call sends nothing (below) |
required lists the variables a call must fill in. A variable with a default is never in it. Other
JSON Schema keywords, such as pattern or nested objects, are refused: a value is always inserted
as text, so there is nothing for them to describe. A number is inserted as you sent it, such as 3
or 2.5, and a boolean as true or false.
Install values¶
A {{ $name }} is an install value: a value you set once in install_values, which Core fills in
before the template is read, so it is part of the instructions themselves. A variable is filled in
by the caller on every call. Each install value says what it is, the same way a parameter of a
shared bundle does:
{"company": {"type": "string", "value": "Acme"},
"max_items": {"type": "number", "value": 5, "description": "how many items to list"},
"tone": {"type": "enum", "value": "formal", "allowed": ["formal", "casual"]}}
type is string, number or enum; an enum lists its allowed values and its value is one
of them. When you share the prompt through a publication, each install value becomes a parameter of
that type with your value as its default, and whoever installs it fills in their own
(Sharing). Every {{ $name }} needs
a value, every value must be used, a name cannot also be a variable, and a value may not contain
{{ or }}.
metadata belongs to the prompt rather than to one version: a new version that leaves it out keeps
what it has, and {} clears it. Put whatever your own application needs there — Core never looks
inside it.
Add a version¶
curl -s -X POST "$CORE_URL/prompts/prm_5Nj2.../versions" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"template": "Classify the ticket below as billing, technical, security or other.\n\n{{ ticket }}",
"input_schema": {"properties": {"ticket": {"type": "string"}},
"required": ["ticket"]}}'
Versions are numbered from 1 and cannot be changed after creation.
| Call | Answers |
|---|---|
GET /prompts |
your prompts, each with its latest version in full |
GET /prompts/{prompt_id} |
one prompt with its latest version |
GET /prompts/{prompt_id}/versions |
its versions, paged by version number, oldest first; add ?order=desc for newest first |
GET /prompts/{prompt_id}/versions/{version} |
one version |
DELETE /prompts/{prompt_id} |
204; every version goes with it |
Copy a prompt¶
To start a new prompt from one you already have, copy it under a new name:
curl -s -X POST "$CORE_URL/prompts/prm_8Kd1.../copy" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "greeting-formal"}'
The answer is 201 with the new prompt, at its own version 1. Name a version to copy that one
instead of the latest. The copy takes the version's template, input schema, role and install
values, and the prompt's labels and metadata. It is your own prompt from then on: changing it never
changes the original. A copy a publication installed sealed, and one whose publication does not
allow forking it ('fork': false), keeps it to its own agents or does not allow reusing it, cannot be copied: 403.
Use a prompt¶
{
"model": "<a model id>",
"prompt": { "id": "triage", "variables": { "ticket": "My card was charged twice." } }
}
Set id to the prompt name or its prm_ id. Each value in variables must fit its property in
the input schema: a number takes a JSON number, not the text "3", and an enum takes one of its
values. Omit version to use the latest version at request
time, or specify it to keep using a particular version after new ones are published.
Core inserts the rendered prompt before the other input items. Stored conversations retain it, so reading the thread shows the instructions used for the turn.
Render a prompt into a tool¶
A prompt can also be the input of a tool rather than of a model. Fix a tool alias's argument to
{"$prompt": {"id": "banner"}} and every call of the alias renders the prompt, fills its
variables from the caller's tool_variables, and passes the text to the tool, for example an
image model. See Fix an argument to a stored prompt.
Template variables¶
The template has the authority of its configured role. A variable's value is labelled untrusted
and is not processed again for placeholders: a value containing {{ ticket }} remains literal
text.
The value is placed inside the template's text, where the placeholder stood. For a key that holds
trust_envelope Core wraps it there in
<untrusted source="prompt-variable" name="...">, so the model is told it is data. For a key
without the capability the value stands in the instruction as plain text, as in any prompt
template, and the model cannot tell it from the words around it.
Use variables for user-supplied content and keep application instructions in the template.
Defaults for variables¶
A variable can have a default that Core fills in when the turn sends no value for it. Give one of two keys in its property:
default: a fixed value of the variable's own type; as text, at most 4096 characters.x-builtin: the name of a value Core knows itself at the moment of the turn, such asdate.
curl -s -X POST "$CORE_URL/prompts" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "helpdesk",
"template": "Today is {{ date }}. Answer {{ tone }}.\n\n{{ question }}",
"input_schema": {
"properties": {"date": {"type": "string", "x-builtin": "date"},
"tone": {"type": "string", "default": "briefly and kindly"},
"question": {"type": "string"}},
"required": ["question"]}}'
A turn that sends only question gives the model Today is 2026-09-24. Answer briefly and kindly.
followed by the question. For each placeholder, Core picks the value in this order:
- the value in the turn's
prompt.variables; - otherwise the variable's
defaultorx-builtin; - otherwise nothing: a required variable then returns
400, an optional one stays empty.
Leave the name out of prompt.variables when you want the default. An empty string is a value too
and wins over the default.
A variable has at most one default, and a variable with a default is never listed in required. A
new version declares its variables the same way. Every read returns the input schema as you wrote
it, completed with "type": "object", "additionalProperties": false, and description as ""
where you left it out.
x-builtin |
What Core fills in |
|---|---|
date |
the date as YYYY-MM-DD, for example 2026-09-24 |
time |
the time as HH:MM, for example 14:05 |
datetime |
date and time in RFC 3339, with the offset from UTC: 2026-09-24T14:05:31+02:00 |
weekday |
the day of the week in English, for example Thursday |
organisation_name |
your organisation's name |
agent_name |
the name of the agent the turn names; empty without an agent |
model |
the model the turn or its agent names; also when the turn then falls over to another model |
subject_id |
the id of the person the turn is for; empty without a person |
subject_name |
the name attribute you send about the person; empty without that attribute |
subject.<attribute> |
what you send about the person for that attribute, such as subject.department; several values joined with , |
thread_id |
the id of the thread the turn is stored on; empty for a turn that is not stored |
run_id |
the id of the background run; empty for a turn you wait for |
What a turn does not have stays empty; the turn is not refused for it. How to send a person and their attributes is described in Build for people.
The four time values are read in the first timezone there is: timezone on the request (Request
fields), the timezone of the schedule firing the
turn, your organisation's timezone (Timezone), and
otherwise UTC.
A default and what Core decides itself -- the time values, agent_name, model, thread_id and
run_id -- carry the template's authority. subject_id, subject_name and subject.<attribute>
come from the caller and are handled as a variable's value is, just like a value in
prompt.variables (above). So is organisation_name,
because people other than the template's author can change that name.
An agent pinning the prompt uses the same defaults: agent.variables need not name a
variable with a default, and agent_name becomes that agent's name.
Errors¶
| Status | Cause |
|---|---|
404 |
no such prompt, no such pinned version, a prompt your key's policy does not name, or prompts is not effective for your key |
403 |
the name you are creating falls outside what your key's policy names; nothing is stored |
409 |
the name is already taken |
422 on template |
a placeholder you did not declare in input_schema |
422 on input_schema |
a declared variable the template does not use, a name in required that is not declared or is listed twice, a variable has one fallback, an x-builtin or a default: <name> (both given), a variable with a fallback is not required: <name> (a default and required), a default or an enum value that does not fit the type, or not a built-in value: <builtin> (a name outside the table above, or an attribute name with anything other than letters, digits, ., _, : and -, or longer than 64 characters) |
422 on install_values |
a {{ $name }} with no value, a value the template does not use, a name that is also a variable, a value containing {{ or }}, or a value that does not fit its type |
400 on prompt.variables |
a required variable with no value on a turn, a value for a variable the prompt does not declare, or a value that does not fit its property (<name> must be a number, <name> must be one of the values its enum lists) |
A refusal of a turn — a request that names a prompt — arrives in OpenAI's error
object, with the field in error.param; a refusal of a write to
/prompts is problem+json, with the field in errors.
Track the version used¶
Each render creates a prompt.rendered audit event with the prompt id,
name and version. Check this event when investigating a change in model behaviour.