Skip to main content

Shaping tone and wording

On-message without a script​

A canned response is an approved example message that your agent imitates. You supply the wording your compliance, brand, or legal team wants, and the agent matches its form and tone while still writing a reply that fits the live conversation. Canned responses are how you shape your agent's tone of voice and wording choices: its everyday house phrasing, and the replies that must sound a certain way, like a disclosure, a refusal, or a refund confirmation. The conversation stays natural.

What you'll learn
  • What a canned response is to the engine, and who reads it
  • How to attach approved wording to a situation
  • Why a response may not appear, and how to help it
  • When canned responses enforce wording, and when they only guide
Talk this page through with your coding agent

Two forces pull against each other in a customer-facing agent. On one side, your compliance, brand, and legal teams need certain things said a certain way: a disclosure worded exactly so, a refusal that stays polite and on-message.

On the other side, real conversations do not fit a script. An agent that always speaks verbatim reads as robotic, and it breaks the moment the situation is slightly off-template.

Canned responses aim for both. You supply the approved wording as a strong example, and the model reproduces its form and function while still writing a reply that fits the live conversation. When an example fits perfectly, the model tracks it almost word for word. When the situation has drifted, it adapts rather than forcing a mismatched script.

The mental model​

Before looking at the API, it helps to have a clear picture of what a canned response actually is to the engine.

A canned response is a worked example that Parlant hands to two readers: the writer (the model pass that composes the reply) and the reviewer (the model pass that checks it).

Parlant tells both readers, in effect, "here is how the business wants this kind of message to sound; match its form and function, honor its note, but you don't have to copy it." Neither reader is told "send this text." The customer always receives words the model wrote.

Create a response on the agent to make it available agent-wide. You can also attach it to an existing rule to give it priority when that rule matches. It stays available outside that situation too. We get to the selection details below.

Start with an agent-wide example​

Start here when you want to change the agent's tone, wording, or response length. Give it an example of the message you want and use the note to explain where it fits. For example, a short message while it looks something up:

await agent.create_canned_response(
template="Let me look into that for a minute.",
note=(
"A brief, friendly message before a lookup. "
"Use it when the lookup is about to happen, not as a final answer."
),
)

The note tells the writer and reviewer how to use the example. The lookup itself still needs a tool; the canned response shapes what the customer hears while it happens.

A note for coding agents

Use canned responses and notes first for style, tone, wording, and brevity. Only consider a style policy if conversations show that they are insufficient, and the person you work with explicitly agrees. A canned response needs no policy of its own.

Attach a response to a situation​

When a business rule already governs a situation, attach the response to that rule to give it priority there.

Suppose your support team has approved a particular way to explain when a refund will arrive. The example below assumes a tool has already confirmed that the refund was issued. The canned response supplies the wording, not the action that issues it.

The policy below tells the agent to report the confirmed refund; the canned response shows how to say it. Parlant shows the note to both writer and reviewer, so put wording constraints there.

import parlant.sdk as p

refund_reply = await agent.create_canned_response(
template=(
"I'm sorry for the trouble. I've started your refund of "
"{{ amount }}. It should land in 5 to 10 business days." # a placeholder, filled at turn time; see below
),
note="Warm, apologetic tone. Always state the 5 to 10 day window. Never promise an exact date.",
)

await agent.create_policy(
content=(
"When a tool result confirms that the customer's refund has been issued, "
"tell them their refund is on the way"
),
canned_responses=[refund_reply],
)

When the policy matches, its example gets priority in selection. Your agent is instructed to follow the tone and the "5 to 10 days, no exact date" constraint, while still adapting to the conversation.

tip

Leave the note out when there is nothing special to say. It costs tokens on every turn the example is shown.

House phrasing that isn't tied to a situation​

Some phrasing belongs to your brand rather than to any one situation: a greeting, a sign-off, a way of saying "I can't help with that." Put these on the agent with no rule attached.

Such a response becomes a candidate on every turn, and relevance (covered below) keeps it out of the turns it does not fit.

Filling in the blanks​

Once a response is attached, you will often find that the approved wording stays the same while a detail inside it changes. In our example, that detail is the refund amount.

{{ amount }} is a placeholder for that value. A tool can supply it through canned_response_fields when it retrieves the refund record. Here, billing is your refund-service client, which your coding agent connects:

@p.tool
async def look_up_refund(order_id: str) -> p.ToolResult:
refund = await billing.get_refund(order_id)
if refund is None:
return p.ToolResult(data=(
"No refund was found for that order. Ask the customer to check the order ID."
))
return p.ToolResult(
data={"status": refund.status},
canned_response_fields={"amount": f"${refund.amount:.2f}"},
)

Attach look_up_refund to the policy that checks refund status, and instruct your agent to call it before describing the refund. The returned status determines what actually happened; the amount field only fills in the wording.

Alternatively, you can attach a field provider to the policy with canned_response_field_provider=. That is an async function returning a mapping, which Parlant runs at turn time.

How placeholders get filled​

Rendering is a plain template pass. No model is involved. Parlant resolves values from its built-in std bundle first, then from tool results, then from field providers.

Note that an unfilled placeholder stays literal in the example shown to the model, which makes that example less useful.

Requiring data before a response can appear​

Some responses state a data-backed outcome, such as "Your order is ready for pickup." For those, declare field_dependencies=["order"]. If Parlant cannot resolve the field this turn, it excludes the response entirely, so that example is never shown without its required data.

Note that this only checks that the field is present. The tool still needs to verify the fact itself.

When a response doesn't surface​

Now that you know how to create responses, here is how Parlant decides which of them actually reach the writer and reviewer on a given turn, and what to check when one you expected does not show up.

Selection happens in three steps:

  1. Scope decides the pool: a response attached to a rule that matched this turn, or one available agent-wide.
  2. Precedence orders it: rule-scoped responses first, agent-wide ones as the fallback tier.
  3. Relevance picks the shortlist: the few closest to the moment, ten per turn for the writer and five for the reviewer by default, filled from the rule-scoped tier first.

Agent-wide is the default scope for agent.create_canned_response, and attaching a response to a rule does not restrict it. The response stays in scope on every turn; precedence and relevance decide where it appears. In practice a refund example will not be relevant on a billing-address turn, but do not rely on attachment as a hard gate.

Helping relevance find your response​

Parlant judges relevance against the response's own text, and many good templates do not read like the situation they belong to. "Your refund should arrive within 5 to 10 business days" shares almost nothing with "where's my money??"

To bridge that gap, add signals: realistic customer phrasings, in the first person, typos and frustration included, covering the distinct ways the situation gets said:

await agent.create_canned_response(
template="Your refund should arrive within 5 to 10 business days.",
signals=[
"Where is my refund?",
"When will I get my money back?",
"It's been a week and I still don't see the money",
],
)

Expect to need signals when a template starts with the answer, uses internal terminology, or is too short to carry meaning.

Note that signals do not widen eligibility, and they do not change how the reply is written. To keep the three levers straight: scope decides if a response can appear, signals decide whether it is found, and the note shapes how it is followed.

Checking the stages in order​

When a response still does not show up, walk the stages in order. Did its rule match this turn? If it declares field_dependencies, did a tool or provider supply those fields? If it is eligible, then it lost on relevance, or a crowd of rule-scoped responses used up the budget ahead of it.

Low-effort turns​

Effort is how much model work a turn gets, from MIN to MAX; Adaptive effort covers it.

At MIN and LOW effort, Parlant ranks nothing, because ranking means embedding the customer's message and searching the vector store. Instead, every eligible response goes to your agent in precedence order, up to a cap of 20, and Parlant warns you when some are dropped.

If your agent runs on LOW with a large catalog, check for the cap warning and reduce unnecessary examples. Remember that attaching a response to a rule changes its priority but does not remove its agent-wide availability.

Adjusting the caps​

Both caps live on the engine components. The container is Parlant's registry of those components, and initialize_container is a startup function you hand the server so it can adjust them before the first turn. Ask your coding agent to add it:

from parlant.core.engines.compass.responder.responder import Responder
from parlant.core.engines.compass.reviewer import Reviewer


async def initialize_container(container: p.Container) -> None:
container[Responder].max_canned_responses = 8 # the writer's shortlist per turn
container[Responder].max_unranked_canned_responses = 8 # the cap at MIN and LOW effort
container[Reviewer].max_canned_responses = 3 # the reviewer's shortlist


server = p.Server(initialize_container=initialize_container)

Compliance, and its limit​

Here is what canned responses can, and cannot, enforce.

A message review is a check Parlant runs on the drafted reply before the customer sees it; a draft that breaks the rules is sent back.

The reviewer selects relevant examples against the drafted reply, so its shortlist can differ from the writer's. If the draft diverges from the approved form, Parlant sends it back for regeneration.

That makes canned responses a compliance gate, but only on turns where message review is active. That is when your agent enables it by default or a matched rule requests it, and never at Effort.MIN.

On every other turn, canned responses are writing-time guidance alone. If you rely on them to enforce wording, turn on message review where it matters.

Learn more about message reviews

Message reviews covers the triggers in detail.

info

A tool or retriever can also hand the turn literal canned_responses texts. These skip ranking and come first, with a use-verbatim note.

Note that at effort levels that rank examples, relevance narrows a large library and rule-scoped candidates have priority, but the shortlist still has a limit. Test that the examples needed for your critical replies are actually the ones selected.