Skip to content

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 as date.
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:

  1. the value in the turn's prompt.variables;
  2. otherwise the variable's default or x-builtin;
  3. 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.