Prompt files¶
A prompt file is YAML. The only required fields are name, description, and either
template or messages.
name: support_reply
description: Drafts a customer support reply in the house style
version: 1.0.0
messages:
- role: system
template: You are a support agent for {{ product }}.
- role: user
template: |
A customer wrote: {{ message }}
Draft a reply.
input_schema:
product: str
message: str
order_id: "str | None"
output_schema: null
metadata:
tags: [support, customer]
author: Olger
model: gpt-4o-mini
A JSON Schema for prompt files is published at
promptkit-core.ochotzas.com/schemas/prompt.schema.json
and ships with the package at promptkit/schemas/prompt.schema.json.
Point your editor at it for validation and autocomplete. In VS Code with the YAML extension:
{
"yaml.schemas": {
"https://promptkit-core.ochotzas.com/schemas/prompt.schema.json": [
"prompts/**/*.yaml",
"!prompts/**/*.evals.yaml"
]
}
}
Or per file, with a modeline at the top of the prompt:
# yaml-language-server: $schema=https://promptkit-core.ochotzas.com/schemas/prompt.schema.json
name: greet
Fields¶
| Field | Required | What it is |
|---|---|---|
name |
yes | Identifier for the prompt |
description |
yes | What the prompt does, for humans |
version |
no | Advisory version string. Not identity — see below |
template |
one of | A single Jinja2 template, loaded as one user message |
messages |
one of | Ordered message templates with roles |
input_schema |
no | Type strings or a JSON Schema object |
output_schema |
no | JSON Schema describing the expected output |
metadata |
no | Tags, author, model hint. Never affects rendering |
Messages¶
messages takes an ordered list. Three forms are accepted:
messages:
- role: system # explicit
template: Be brief.
- user: Hello {{ name }} # shorthand
- Hello again # bare string, becomes a user message
Valid roles are system, user, and assistant. Anything else is rejected at load.
A message that renders to only whitespace is dropped, which makes conditional messages easy:
messages:
- role: system
template: "{% if tone %}Write in a {{ tone }} tone.{% endif %}"
- role: user
template: "{{ question }}"
The single-template form¶
This is loaded as one user message. It is fully supported, not deprecated — use it
when a prompt genuinely has no system instruction.
Templating¶
Templates are Jinja2 with two deliberate settings:
StrictUndefined— an unprovided variable is an error, never an empty string. Silent empty interpolation is the exact bug PromptKit exists to prevent.- Sandboxed — templates cannot reach attributes like
__class__or call arbitrary Python, so rendering a prompt file you did not write is safe.
Everything else works: {% for %}, {% if %}, filters, {% include %}.
template: |
Summarise these items:
{% for item in items %}
- {{ item.title }} ({{ item.count }})
{% endfor %}
{% if audience %}Write for {{ audience }}.{% endif %}
Version and fingerprint¶
version is advisory metadata for humans. Identity is Prompt.fingerprint, a hash
over the messages, schemas, and resolved includes.
a = load_prompt("greet.yaml") # version: 1.0.0
b = load_prompt("greet-copy.yaml") # version: 9.9.9, same content
assert a.fingerprint == b.fingerprint # version is not identity
This is what makes caching correct: editing a prompt or one of its partials changes the
fingerprint, which invalidates cached responses. Bumping version alone does not.
Loading and saving¶
from promptkit import load_prompt, loads_prompt, save_prompt
prompt = load_prompt("prompts/greet.yaml")
prompt = load_prompt("prompts/greet") # extension optional
prompt = loads_prompt(yaml_text) # from a string
save_prompt(prompt, "prompts/greet.yaml")
save_prompt writes template: for a single user message and messages: otherwise, so
files round-trip in the shape they were written.
Directories¶
A directory of prompts becomes a registry with dotted names:
prompts/
├── _partials/
│ └── house_style.j2 partial, not a prompt
├── greet.yaml -> greet
├── greet.evals.yaml eval suite, not a prompt
└── support/
└── refund.yaml -> support.refund
from promptkit import PromptRegistry
registry = PromptRegistry("prompts")
registry.names() # ['greet', 'support.refund']
registry.get("support.refund")
Directories prefixed with _ hold partials. Files ending in .evals.yaml are eval
suites. Neither is loaded as a prompt.
The registry caches by mtime, so editing a file on disk is picked up without a restart.
Versions¶
When several files declare the same prompt name with different version values, pin
one with @:
registry.get("support.refund") # by path, as usual
registry.get("support.refund@2.1.0") # a specific version
registry.get("support.refund@latest") # the highest version
registry.versions("support.refund") # {'1.0.0': Path(...), '2.1.0': Path(...)}
Versions sort numerically, so 2.10.0 correctly beats 2.1.0. Asking for a version that
does not exist lists the ones that do.