Customizing prompts
Changing what the built-in prompts say
Prompt customization lets you change one part of the prompts Parlant writes for itself, without replacing the whole prompt. Parlant has its own prompts for responding, matching rules, reviewing, and compacting. Most of the time you never touch them: what your agent should do belongs in your instructions and policies. You use this when a built-in principle, such as "be concise", conflicts with your brand or a wording rule.
- The trade-off you accept when you replace a built-in section
- How prompts are split into named sections, and how a builder intercepts one
- A worked example: replacing the responder's general instructions
- Which routes carry prompts, and how to find a section's name
The trade-off
What you are trading here is ownership. Parlant's wording is tuned and tested against its own components, so a section you replace stops receiving those improvements. It can also shift behavior in ways that only your own testing will catch.
Our advice is simple: replace the one section that is wrong for you, keep it small, and re-test whenever you change a model or an override.
How it works
You do not need to replace an entire prompt to change one instruction. Parlant divides each of its prompts into named sections, so you can change the relevant part and keep the rest.
A prompt section is one block of a built-in prompt, such as the responder's task description. Each section has a stable name, a template, and props (the values Parlant fills into the template's {placeholder} slots when it builds the prompt).
Intercepting a section
To customize a section, your coding agent subclasses p.PromptBuilder and overrides adapt_section. Parlant calls it once per section, passing the section's name, template, and props. Your override returns one of three verdicts:
Nonekeeps the section unchanged. This is the default for every section you do not match.- A string replaces the section's template. Keep every
{placeholder}the original uses, because Parlant supplies their values through props. A placeholder you drop is content you drop, and a placeholder that is missing fails loudly at build time. p.VETOdrops the section entirely.
Registering the builder
Once you have a builder, you register it under a route with prompts={...}. A route is the fixed name for one kind of work in a turn: react for generating the reply, review.message for checking a draft, and so on.
Note that prompts uses the same route keys as models. Routes decide both which model a component calls and which prompt builder it uses.
Your own general instructions
The most common customization is replacing the responder's general instructions. These are the platform-level principles that every reply follows.
For example, a private-banking agent may need to be formal where the default says conversational. A regulated business may have to answer in one language where the default follows the customer's.
First, define MY_GENERAL_INSTRUCTIONS from the original section's template. Preserve its placeholders and change only the instructions you intend to own.
You can inspect the built prompt in a diagnostic report, which is the easiest way to find the original section's template.
The section to match is p.PromptSections.Responder.TASK_DESCRIPTION. You register the builder on the react route, since that is the route the responder runs on:
import parlant.sdk as p
class MyResponderPrompt(p.PromptBuilder):
def adapt_section(self, name, template, props):
if name == p.PromptSections.Responder.TASK_DESCRIPTION:
return MY_GENERAL_INSTRUCTIONS # your template, keeping the original's placeholders
return None # every other section stays as it is
async with p.Server(
models={"*": "openai"},
prompts={"react": MyResponderPrompt}, # applies to the responder's prompts only
) as server:
...
Note that you should not override add_section itself. Its signature is internal and free to change. adapt_section is the stable extension point.
Which prompts to customize
The keys in prompts accept the same selectors as models: concrete routes, namespace wildcards such as "review.*", "*" for everything, and effort qualifiers such as "matching.rank[effort=high]".
An effort qualifier restricts the entry to turns at a given effort, the level from MIN to MAX that decides how much model work a turn gets. Within a scope, the most specific selector wins.
These are the routes that carry prompts:
| Route | Prompt |
|---|---|
react | The responder, both its system instructions and its per-turn instructions. |
matching.rank | Per-rule turn evaluation and discovered-rule pruning. They share a route; tell them apart by section name. |
review.message / review.tools | The two review prompts. |
compaction | Session compaction. |
Note that embedding and tokenizer routes carry no prompts.
You can also register a builder per agent with create_agent(prompts={...}). When you do, agent scope wins over server scope.
Finding the section names
To match a section, you need its name. Every section name is registered, stable API, collected under p.PromptSections: Responder, Ranker, Pruner, Reviewer (with MESSAGE_* and TOOLS_* members), Compacter, and MatchingCommon for fragments that several prompts share.
Match on the enum members, not on raw strings. A test in the repository guarantees the enums are complete.
Note that sections added by the shared builder helpers (agent identity, terms, interaction history) carry p.PromptBuilder's built-in section names. adapt_section sees those too, so you can reword or veto them the same way.
Rules to follow
Three rules keep a builder safe and cheap.
Be deterministic. Parlant arranges each prompt so that the provider can cache its unchanging prefix and only pay for the live tail. That cache keys on exact bytes.
A builder whose output changes between calls (through timestamps, random ordering, or mutable closures) silently disables prompt caching and raises your costs. Customize how the prompt reads, not per-call content. Per-call content comes through props.
Keep it fast. Your builder runs on every prompt construction, so do expensive preparation at import time.
Trace logging is preserved. Parlant attaches its own build listener to whatever your builder returns, so prompt traces keep working without you forwarding anything.