# Prompt Customization
This guide covers customizing SubAgent system prompts for different LLMs, execution modes, and use cases.
## Execution Modes
SubAgent prompts are built from a 2-axis architecture:
**Behavior axis** — how the agent returns results:
| Behavior | Description | When to use |
|----------|-------------|-------------|
| `:single_shot` | Last expression IS the answer | `max_turns: 1`, simple queries |
| `:explicit_return` | Must call `(return ...)` or `(fail ...)` | Multi-turn exploration with tools |
**Reference** — language documentation (tool syntax, Java interop, restrictions):
The language reference is included by default. Use `reference: :none` to omit it for capable models that don't need syntax guidance.
**Additive capabilities:**
| Capability | Description | When to use |
|------------|-------------|-------------|
| `:journal` | Adds `task`, `step-done`, `task-reset` docs | Plan-driven agents with idempotent steps |
### Default Selection
SubAgent automatically selects a prompt based on configuration:
| Condition | Default Language Spec |
|-----------|----------------------|
| `max_turns <= 1` | `:single_shot` |
| `journaling: true` | `:explicit_journal` |
| Otherwise | `:explicit_return` |
**Note:** `plan:` provides display-only progress labels and does not affect language spec selection. To enable journal capabilities (`task`, `step-done`), set `journaling: true` explicitly.
You rarely need to set this manually — the defaults match the runtime behavior.
## Canonical Language Specs
Pre-composed specs available via `system_prompt: %{language_spec: atom}`:
| Spec | Components | Description |
|------|------------|-------------|
| `:single_shot` | reference + single-shot | Last expr = answer, one turn |
| `:explicit_return` | reference + multi-turn + explicit return | Must call `(return ...)`/`(fail ...)` |
| `:explicit_journal` | reference + multi-turn + explicit return + journal | With task caching |
The language reference is included by default. Use `reference: :none` in a structured profile to omit it.
```elixir
# Single-turn query (default for max_turns: 1)
SubAgent.new(
prompt: "Count items over $100",
max_turns: 1
)
# Multi-turn exploration (default for max_turns > 1)
SubAgent.new(
prompt: "Analyze sales trends",
max_turns: 5
)
# Omit language reference for capable models
SubAgent.new(
prompt: "Count items",
max_turns: 1,
system_prompt: %{language_spec: {:profile, :single_shot, reference: :none}}
)
```
## Structured Profiles
For programmatic composition, use the `{:profile, behavior, opts}` tuple:
```elixir
# Omit language reference for a capable model
SubAgent.new(
prompt: "Execute the plan",
system_prompt: %{
language_spec: {:profile, :explicit_return, reference: :none}
}
)
# Add journal capability
SubAgent.new(
prompt: "Execute the plan",
system_prompt: %{
language_spec: {:profile, :explicit_return, journal: true}
}
)
# Both reference (default) and journal
system_prompt: %{language_spec: {:profile, :explicit_return, journal: true}}
# Short form (defaults: reference: :full, journal: false)
system_prompt: %{language_spec: {:profile, :explicit_return}}
```
**Options:**
| Option | Values | Default |
|--------|--------|---------|
| `:reference` | `:full` or `:none` | `:full` |
| `:journal` | `true` or `false` | `false` |
**Validation:** Raises `ArgumentError` for invalid combinations (e.g., `single_shot + journal`).
## System Prompt Customization
The `system_prompt` field accepts three forms:
```elixir
# Map with options
system_prompt: %{
prefix: "You are an expert data analyst.",
suffix: "Always validate results before returning.",
language_spec: :explicit_return,
output_format: "..."
}
# Function transformer
system_prompt: fn prompt -> "CUSTOM PREFIX\n\n" <> prompt end
# Complete override (use with caution)
system_prompt: "Your entire custom prompt here..."
```
### Map Options
| Option | Description |
|--------|-------------|
| `:prefix` | Prepended before generated content |
| `:suffix` | Appended after generated content |
| `:language_spec` | Replaces PTC-Lisp reference section (atom, string, tuple, or callback) |
| `:output_format` | Replaces output format instructions |
## Dynamic Language Spec
Use a callback to change prompts based on runtime context:
```elixir
SubAgent.new(
prompt: "Process the data",
system_prompt: %{
language_spec: fn ctx ->
if ctx.turn == 1 do
PtcRunner.Lisp.LanguageSpec.get(:single_shot)
else
PtcRunner.Lisp.LanguageSpec.get(:explicit_return)
end
end
}
)
```
The callback receives:
| Key | Type | Description |
|-----|------|-------------|
| `:turn` | integer | Current turn number (1-indexed) |
| `:model` | atom or function | The LLM |
| `:memory` | map | Current memory state |
| `:messages` | list | Conversation history |
## LLM-Specific Prompts
Different models may need different prompt styles. Capable models may work fine without the language reference, saving tokens:
```elixir
defmodule MyApp.Prompts do
alias PtcRunner.Lisp.LanguageSpec
def for_model(ctx) do
case ctx.model do
model when model in [:sonnet, :gpt4o] ->
# Capable models: omit language reference to save tokens
LanguageSpec.resolve_profile({:profile, :explicit_return, reference: :none})
_ ->
# Default: include language reference
LanguageSpec.get(:explicit_return)
end
end
end
SubAgent.new(
prompt: "Analyze orders",
system_prompt: %{language_spec: &MyApp.Prompts.for_model/1}
)
```
## Custom Prompt Addons
Build on top of library prompts:
```elixir
defmodule MyApp.Prompts do
alias PtcRunner.Lisp.LanguageSpec
def with_domain_context do
"""
#{LanguageSpec.get(:single_shot)}
## Domain Context
- Orders have statuses: pending, shipped, delivered, cancelled
- Products belong to categories: electronics, clothing, food
- Use `data/current_user` for permission checks
"""
end
end
SubAgent.new(
prompt: "Find high-value orders",
system_prompt: %{language_spec: MyApp.Prompts.with_domain_context()}
)
```
## Prompt Preview
Inspect the generated prompt without execution:
```elixir
agent = SubAgent.new(
prompt: "Find emails for {{user}}",
system_prompt: %{
prefix: "You are a helpful assistant.",
language_spec: :explicit_return
}
)
preview = SubAgent.preview_prompt(agent, context: %{user: "alice"})
IO.puts(preview.system) # Full system prompt
IO.puts(preview.user) # Expanded user prompt
```
## Text Mode Templating
Text mode uses full Mustache templating with sections for iterating lists. This differs from PTC-Lisp mode where data appears in the Data Inventory section.
See [Text Mode Guide](subagent-text-mode.md) for Mustache syntax including `{{#section}}`, `{{^inverted}}`, and `{{.}}` notation.
## See Also
- [Text Mode Guide](subagent-text-mode.md) - Mustache templates, structured output, and native tool calling
- [Core Concepts](subagent-concepts.md) - Context and memory
- [Advanced Topics](subagent-advanced.md) - System prompt structure details
- [Benchmark Analysis](../benchmark-eval.md) - Statistical testing of prompt variants
- `PtcRunner.Lisp.LanguageSpec` - Full API reference for language specs and profiles
- `PtcRunner.SubAgent.SystemPrompt` - Prompt generation internals