# dot-prompt
A compiled language for LLM prompts. Define structure, branching, and contracts in `.prompt` files — ship clean prompts to your LLM.
---
## The Problem
Every team building with LLMs ends up in the same place. Prompts scattered across the codebase as f-strings, markdown files, or YAML configs. Branching logic tangled into application code. No versioning. No contracts. No tooling. Token waste invisible. The LLM receives everything — including all the logic you meant to resolve before the call.
```python
# What most teams end up with
prompt = f"""
You are a {role}.
{"Answer the question directly." if is_question else "Continue the lesson."}
{"Give a short answer." if depth == "shallow" else "Give a detailed answer."}
Here is the context: {context}
The user said: {user_message}
"""
```
This works until it doesn't. Then it's very hard to fix.
---
## The Solution
`.prompt` files are compiled before they reach the LLM. Branching resolves at compile time. The LLM receives a clean, flat string with zero logic residue.
```
init do
@version: 1.0
@major: 1
def:
mode: explanation
description: Teacher mode — explanation phase.
params:
@pattern_step: int[1..5] = 1 -> current step in the teaching sequence
@variation: enum[analogy, recognition, story]
-> teaching track — required, selected once per session
@answer_depth: enum[shallow, medium, deep] = medium -> depth of answers
@if_input_mode_question: bool = false
-> true when user has asked an off-pattern question
@user_input: str -> the user's current message
@user_level: enum[beginner, intermediate, advanced] = intermediate
fragments:
{skill_context}: static from: skills
match: @skill_names
end init
if @if_input_mode_question is true do
STOP TEACHING. Answer the user's question directly.
The user asked: @user_input
case @answer_depth do
shallow: Shallow Answer
1-2 sentences answering exactly what they asked.
medium: Medium Answer
Explanation + 1 relevant example.
deep: Deep Answer
Full explanation with multiple examples.
end @answer_depth
response do
{
"response_type": "question_answer",
"content": "your response here",
"ui_hints": { "show_answer_input": false }
}
end response
else
case @variation do
analogy: #Analogy Track
case @pattern_step do
1: Opening Anchor
Introduce the concept with a single real-world analogy.
2: Deepening the Frame
Build on the analogy. Layer in the formal definition.
3: Concrete Examples
Give 2 examples. First obvious, second subtle.
end @pattern_step
recognition: #Recognition Track
case @pattern_step do
1: Opening Anchor
Open with a question that makes the user realise they already use this concept.
2: Deepening the Frame
Return to their recognition. Use their words to introduce the formal framing.
3: Concrete Examples
Ask the user to generate their own example first.
end @pattern_step
end @variation
@user_input
response do
{
"response_type": "teaching",
"content": "your response here",
"ui_hints": { "show_answer_input": true }
}
end response
end @if_input_mode_question
```
**What the LLM receives** for `variation: recognition`, `pattern_step: 2`, `answer_depth: medium`, `if_input_mode_question: false`:
```
Deepening the Frame
Return to their recognition. Use their words to introduce the formal framing.
[user message]
Respond with this JSON:
{
"response_type": "teaching",
"content": "your response here",
"ui_hints": { "show_answer_input": true }
}
```
No branching. No logic. No dead weight. Just the instruction the LLM needs.
---
## Features
**Compiled language** — branching resolves before the LLM call. `if`, `case`, and `vary` blocks compile away entirely. The LLM never sees them.
**Input and output contracts** — params declare the input contract. `response` blocks declare the output contract. Both are versioned together. Breaking changes are detected automatically.
**Fragment composition** — `.prompt` files compose. Static fragments are cached. Dynamic fragments are fetched fresh. Collections load multiple fragments from a folder and composite them.
**Variation tracks** — `vary` blocks select branches randomly or by seed. One seed drives all vary blocks in a prompt deterministically.
**Semantic versioning** — `@major` pins the contract version. Callers pin to a major version and receive non-breaking updates automatically. Old major versions are served from `archive/` for callers that have not upgraded.
**Breaking change detection** — the container detects breaking contract changes on every save. Prompts the developer to version before committing. Hard warning at git commit if unversioned breaking changes exist.
**Snapshot safety** — the container snapshots every `.prompt` file before the first edit after a commit. LLM agents can edit freely — the pre-edit baseline is always preserved for archiving.
**MCP server** — LLM coding tools discover prompt schemas, params, and contracts via MCP without reading raw files.
**Works with any language** — Elixir gets a native library. Everyone else calls the container HTTP API.
---
## How It Works
```
.prompt file + params
│
▼
[Stage 1] Validate params against declared types
│
▼
[Stage 2] Resolve if/case — discard untaken branches
← structural cache by compile-time params
│
▼
[Stage 3] Expand fragments — compile static, fetch dynamic
← fragment cache by path + params
│
▼
[Stage 4] Resolve vary slots — seed or random selection
← vary branch cache preloaded at startup
│
▼
[Stage 5] Inject runtime variables
│
▼
DotPrompt.Result { prompt: "...", response_contract: %{...} }
```
Three independent cache layers. The structural skeleton is cached by compile-time params. Vary branches are preloaded at startup. Fragment content is cached by path and version. Runtime variables are injected fresh every call.
---
## Elixir Library Usage
Add to your `mix.exs`:
```elixir
defp deps do
[
{:anantha_dot_prompt, "~> 1.1"}
]
end
```
Configure the prompts directory:
```elixir
config :anantha_dot_prompt,
prompts_dir: Path.expand("../prompts", __DIR__)
```
Usage:
```elixir
# List available prompts
DotPrompt.list_prompts()
# Get prompt schema
{:ok, schema} = DotPrompt.schema("router")
schema.params # map of declared params
# Render a prompt with params
{:ok, result} = DotPrompt.render("memory/extract/claims", %{actor_name: "Ramesh"}, %{})
result.prompt # compiled string sent to LLM
# Compile and inject separately
{:ok, compiled} = DotPrompt.compile("my_prompt", params)
final = DotPrompt.inject(compiled.prompt, %{user_input: "hello"})
```
---
## Language Reference
### The One Rule
`@` means variable. Always. Only. Everywhere.
Structural keywords never use `@`.
### Init Block
```
init do
@major: 1
@version: 1.0
def:
mode: explanation
description: Human readable description.
params:
@name: type = default -> documentation
fragments:
{name}: static from: folder_or_file
{{name}}: dynamic -> fetched fresh each request
docs do
Free text documentation. Surfaces via MCP.
end docs
end init
```
### Types
| Type | Lifecycle | Notes |
| ------------- | ------------ | ---------------------- |
| `str` | Runtime | Cannot drive branching |
| `int` | Runtime | Cannot drive branching |
| `int[a..b]` | Compile-time | Bounded integer |
| `bool` | Compile-time | |
| `enum[a, b, c]` | Compile-time | Single value |
| `list[a, b, c]` | Compile-time | Multiple values |
### Control Flow
```
if @var is x do # equality
if @var not x do # inequality
if @var above x do # greater than
if @var below x do # less than
if @var min x do # greater than or equal
if @var max x do # less than or equal
if @var between x and y do # inclusive range
elif @var is x do # chained condition
else # fallback
end @var
case @var do # deterministic branch selection
value: Title
content here
end @var
vary @var do # random or seeded — enum required
branch_name: content here
end @var
```
### Fragments
```
fragments:
{single}: static from: skills
match: @skill # enum — returns one
{multi}: static from: skills
match: @skill_names # list — returns composited
{pattern}: static from: skills
matchRe: @skill_pattern # enum of regex patterns
{all}: static from: skills
match: all # every file in folder
limit: 10
order: ascending
{{live}}: dynamic # fetched fresh each request
```
### Response Contract
```
response do
{
"field": "value",
"nested": { "bool_field": true }
}
end response
```
Compiler derives contract schema from JSON structure.
Multiple response blocks compared across branches — warning if compatible, error if incompatible.
### Sigils
| Sigil | Meaning |
| -------- | -------------------------------- |
| `@name` | Variable |
| `{name}` | Static fragment |
| `{{name}}` | Dynamic fragment |
| `#` | Comment — never reaches LLM |
| `->` | Documentation — surfaces via MCP |
| `=` | Default value |
---
## Versioning
```
init do
@major: 1 # contract version — callers pin to this
@version: 1.3 # major.minor — managed by container
end init
```
**Breaking changes** — removing or renaming params, changing types, removing response fields — require `@major` to increment. The old version is archived. Callers pinned to the old major continue to be served.
**Non-breaking changes** — adding params with defaults, changing docs, internal prompt edits — auto-bump `@minor` on commit. Callers never notice.
---
## License
Apache 2.0