Conditional prompt sections
Conditional sections keep or drop parts of an agent's prompt for each call, based on its variables: text inside {#if {plan} == 'gold'} … {/if} reaches the model only when the condition holds.
They build on context variables. A variable fills in a value ({customer_name}); a section decides which instructions a call gets. They work in a single-context agent's system prompt, in a flow agent's shared prompt, and in each flow step's own prompt.
Why use them
- Shorter prompts. Instructions that don't apply to this call are removed before the model sees them, so each turn sends fewer tokens. That means lower cost and less delay.
- No contradictory instructions. The model never reads "if they are new, do X; if they are returning, do Y" and then picks the wrong branch. It only gets the branch that applies.
- Fewer flow steps. You don't need one copy of a step for new callers and another for returning ones, or one per plan. A single step can hold a conditional section.
A first example:
You are the renewals agent for {company}. Keep replies short.
{#if {is_existing_customer} == true}
They are a returning customer. Greet them by name and don't re-explain the plans.
{#elif {plan} == 'trial'}
They are on a trial. Ask how the trial went before anything else.
{#else}
Explain the three plans briefly, then qualify.
{/if}
Never quote a price that is not in the knowledge base.
A call that starts with company = "Acme" and is_existing_customer = "true" gives the model exactly this:
You are the renewals agent for Acme. Keep replies short.
They are a returning customer. Greet them by name and don't re-explain the plans.
Never quote a price that is not in the knowledge base.
A call with plan = "trial" and no is_existing_customer gets the trial line instead, and a call with neither gets the "Explain the three plans" line. The tag lines themselves never reach the model.
Syntax
| Tag | Meaning |
|---|---|
{#if CONDITION} | Opens a section. Its text is kept when the condition holds. |
{#elif CONDITION} | Another branch, tried in order when nothing above it held. Use as many as you need. |
{#else} | The last branch, kept when no condition held. Optional, at most one per section, and it must come after every {#elif}. |
{/if} | Closes the section. Every {#if} needs its own. |
The rules:
- Put each tag on its own line. When a tag is alone on its line (only spaces or tabs around it), the whole line is removed, line break included, so a skipped section leaves no blank gap. A tag inside a sentence also works: it is removed in place, and nothing else around it changes.
Greet them{#if {vip} == true} warmly{/if}.becomesGreet them warmly.orGreet them. - A tag opens and closes on one line.
{#ifand the}that ends its condition must be on the same line. - Lowercase, exactly as shown.
{#ifand{#elifare followed by a space, then the condition.{#else}and{/if}take nothing else. - Sections can nest, to any depth.
{/if}always closes the most recent{#if}that is still open. - Each field balances on its own. A section that opens in a flow's shared prompt must close there too, not in a step's prompt, and each step's prompt is checked separately.
{variables}inside a branch are filled in as usual, after the branch is chosen.- To write a literal
{#if, double the brace:{{#ifshows as{#if, the same escape{{/}}uses everywhere in prompts. The builder will show a "Handlebars syntax" warning for it, which you can ignore when the literal text is what you want.
Nesting example: the inner section is only considered when the outer one is kept.
{#if {plan} == 'business'}
Offer the account manager callback.
{#if {seats} >= 50}
Mention the volume discount.
{/if}
{/if}
Conditions
A condition compares a variable with a value, using the same rules as a flow's equation exits. A section and an exit with the same text always agree.
| Operator | Means | Builder picker | Example |
|---|---|---|---|
== | equals | is | {tier} == 'gold' |
!= | does not equal | is not | {tier} != 'gold' |
>, < | greater than, less than (numbers) | is greater than, is less than | {balance} > 1000 |
>=, <= | at least, at most (numbers) | is at least, is at most | {age} >= 18 |
!= '' | has a value | has any value | {order_id} != '' |
== '' | is empty or was never set | is empty or unset | {order_id} == '' |
contains | the text contains (ignores case) | contains | {notes} contains 'refund' |
starts_with | the text starts with (ignores case) | starts with | {phone} starts_with '+91' |
The variable goes in braces, {name}. A bare word is text, so plan == 'gold' compares the word plan and never matches.
The builder's Add condition picker offers the number operators only for a variable typed Number, and contains / starts with only for text (not for Number or Yes / no). Typed by hand, every operator works on any variable.
Examples by kind of value
- Text:
{tier} == 'gold'.==and!=compare text exactly, including case:'Gold'does not equal'gold'. If the values come from a fixed list, declare the variable a Choice (a value matches its choice whatever its case, see Typed variables), or usecontains, which ignores case. - Number:
{age} >= 18.>,<,>=and<=only hold when both sides read as numbers;"1,000"or"eighteen"is not a number, so the comparison is false.==also compares numbers as numbers when both sides read as numbers ("007" == 7holds). - Yes/no:
{is_existing_customer} == true. This matches the texttrueexactly."TRUE"and"1"match only when the variable is typed Yes / no (see below);"yes"never does. - Choice:
{plan} == 'Pro', spelled exactly as the choice is declared. - Has a value / is empty:
{order_id} != ''and{order_id} == ''. - Part of the text:
{notes} contains 'refund',{phone} starts_with '+91'. Both ignore case. The words are lowercase with a space on each side, and an empty value after them (contains '') is always false. - Two variables:
{paid} >= {due}compares two variables.
Combining conditions
Join comparisons with && (and) and || (or):
{#if {plan} == 'gold' && {city} == 'Mumbai'}
{#if {intent} == 'billing' || {intent} == 'refund'}
{#if {age} > 18 && {age} < 65}
&&binds tighter than||.{a} == 1 || {b} == 2 && {c} == 3means "a is 1, or both b is 2 and c is 3". The builder warns when one condition mixes them.- No parentheses. For other grouping, nest one section inside another.
- One comparison per pair. Write
{age} > 18 && {age} < 65, never18 < {age} < 65. - Always double:
&&and||. A single&or|is an error. - No
and,or,not,=,===or!{x}. Use&&,||,!=and==; "is empty" is{x} == ''.
Quoting values
Put text values in straight quotes, single or double: 'gold' or "gold". Numbers and true / false need none.
Quote any value that contains spaces, punctuation or symbols: {city} == 'New Delhi'. Without quotes, a value containing &, |, (, =, <, > or the words and, or, not is misread. If a value contains an apostrophe, use double quotes: {surname} == "O'Brien". Curly quotes (‘gold’, “gold”, which word processors and chat apps insert) are not quotes here, so the builder refuses them.
When conditions are decided
This is the most important thing to understand about sections. A section is decided once, from the values the call holds at that moment, and is not decided again later.
| Where the section is | Decided | With which values |
|---|---|---|
| A single-context agent's system prompt | Once, when the call starts | The values the call starts with (see below) |
| A flow agent's shared prompt (the Global prompt) | Once, when the call starts | The same start values |
| A flow step's own prompt | Each time the call enters that step | Everything the call holds at that moment, including what earlier steps captured |
| Agent B's prompt, after an Agent Transfer hands the call over | When the handoff happens | The call's live values, including what agent A captured |
Start values are the variables sent with an API call, a campaign contact's row, values from the widget or the test panel, an inbound call's pre-call lookups (they run before the prompt is decided), the agent's declared defaults, the built-in date and time variables, and {caller_channel}. When Send caller identity to tools is on, {caller_number} and {caller_identifier} are included too. {session_id} is empty in a prompt decided at call start.
What this means in practice:
- A value captured during the call never changes a single-context prompt. If a tool, a keypad entry or an extract step sets
{otp_verified}mid-call, a single-context section on{otp_verified}was already decided at the start, usually on an empty value. To branch on something learned during the call, use a flow agent and put the section in the prompt of a step that comes after the capture. - In a flow, put sections that depend on captured values in step prompts, not in the shared prompt. The shared prompt is decided before any step runs. The builder warns when a shared-prompt condition reads a variable that only a step writes.
- A step reached right after the capture sees the value. When a step's prompt reads a variable the step being left is capturing, the platform finishes the capture before it decides the arriving step's sections.
- A value that changes while the call is on a step takes effect at the next step entry. A step the call enters again (a loop, a menu) is decided again on each entry.
When a variable is missing
A variable that was never supplied compares as empty text:
| Condition | Missing variable |
|---|---|
{x} == 'a' | false |
{x} != 'a' | true |
{x} > 3 (and <, >=, <=) | false |
{x} contains 'a', {x} starts_with 'a' | false |
{x} == '' | true |
{x} != '' | false |
!= is the trap. {#if {plan} != 'free'}…premium instructions…{/if} includes the premium text on every call that doesn't send plan. The builder warns about != on an optional variable that has no default. To be safe, do one of these:
- Add an
{#else}that says what to do when nothing matched. - Mark the variable required, so a call without it is refused before it starts.
- Give it a default, so a call without it uses the default.
- Test with
==, which is false for a missing value.
Typed variables
Values often arrive in different forms that mean the same thing: true, TRUE and 1; Gold and gold ; a CSV cell with a trailing space. == compares text exactly, so {vip} == true is false for TRUE. Giving a variable a type fixes this. When the call starts, each value is rewritten to one standard spelling, so a condition written against that spelling matches every form of it.
A type also changes what the builder offers: the condition picker gives a Yes / no variable true / false, a Choice variable its list of choices, and a Number variable the number operators. The Test variables card shows a Yes / no or Choice variable as a dropdown.
| Type (in the builder) | type | What happens to a value when the call starts | Write conditions as |
|---|---|---|---|
| Untyped | (absent) | Used exactly as supplied | Exact text |
| Text | text | Spaces around it are trimmed | {city} == 'Pune' (still case-sensitive) |
| Number | number | Trimmed. It is still compared as a number whenever it reads as one | {age} >= 18 |
| Yes / no | boolean | true or 1 becomes true; false or 0 becomes false, in any case. Anything else is kept as given, trimmed | {vip} == true or {vip} == false |
| Choice | choice, with choices | A value that matches one of the choices, ignoring case, surrounding spaces and Unicode composition, is stored as that choice exactly as you wrote it. A value that matches none is kept as given, trimmed | {plan} == 'Pro', spelled as declared |
Things to know:
- Words are never translated.
yes,haanorouistay as they are for a Yes / no variable, so== trueis false for them. Onlytrue,false,1and0are converted. That keeps the platform language-neutral; map your own words totrue/falsebefore you send them. - The standard spelling is what the whole call sees. Exits, tools and
{placeholders}get the same rewritten value as the sections, not the raw input. - A value that is only spaces becomes empty. A required variable then counts as missing.
- Only start values are rewritten. A value captured during the call (an extract step, a keypad entry, a tool's or code step's output) is stored and compared exactly as captured.
- Typing a variable can break existing conditions. After you give
planthe choicesGoldandSilver, a valuegoldis stored asGold, so an existing{plan} == 'gold'stops matching. The same is true of{vip} == 1oncevipis Yes / no. The builder lists every condition the new type would stop matching on the variable's row, naming where it is written, and the prompt's warnings include it (section_literal_vs_type). Change those conditions to the standard value.
Setting a type. In the builder, open the Variables panel: the Variables button on the toolbar, or, in a single-context agent, Set types and defaults under its context variables. Pick the type next to each declared variable; for Choice, add its choices. Over the API it is metadata.variables[].type and choices (field reference):
{ "name": "plan", "required": false, "default": "Basic",
"type": "choice", "choices": ["Basic", "Pro", "Enterprise"] }
An unknown type ("string", "Boolean") reads as untyped, and choices that is not a list of strings is ignored. Neither is an error.
In the builder
Highlighting and auto-close
In every prompt field (the single-context system prompt, a flow's shared prompt, Conversation and Subagent step prompts):
- Section tags are tinted light blue, distinct from the grey shading of
{variables}. - With the cursor inside a section, that section's tags (
{#if}, any{#elif}/{#else}, and{/if}) are highlighted together, so you can see which{/if}closes which{#if}. - A tag that is wrong where it stands is rose with a wavy underline.
- Auto-close: when you type the
}that ends an{#if …}tag that has no{/if}yet, a{/if}is added on the next line and the cursor stays where it was. Only typing triggers it, not pasting or deleting, and Undo removes it.
In a field that is spoken aloud (greetings, openers, step entry lines, Say and menu lines, closing lines), every tag is shaded as an error, with a warning that it would be read out.
The problems list
Under each prompt field, each problem is listed on its own row with its line, for example "Line 3: {#if is never closed — add {/if} where the section should end." Errors are red; look-alike tags (warnings) are amber. Click a row to put the cursor on that line. A long list shows five rows and a Show all button.
The list under the field covers structure errors and look-alike tags. The other warnings (an unknown variable, != on an optional one, a typed variable compared with a value it never holds, …) are in Preview with values for a single-context or shared prompt, and in POST /api/agents/flow/validate for step prompts.
Publishing with a broken section
While a prompt field has a structure error, Publish is disabled, and its tooltip names the first problem. The server would refuse the save anyway (see Over the API). Save draft still works, so a half-written section can wait. The one exception is a brand-new flow agent: its first draft creates the agent from the shared prompt, so the shared prompt has to be well-formed.
An agent saved before sections existed may already contain {#if as plain text. That doesn't block publishing your other changes: only a problem the live agent doesn't already have blocks it.
Add condition
Add condition is on every prompt field, in the prompt's toolbar or just under the field. Select the text that should depend on a condition (or put the cursor where an empty section should go), then build the condition from pickers:
- a variable, an operator (is, is not, has any value, is empty or unset, contains, starts with, is greater than, …) and a value. A Yes / no variable offers yes / no and a Choice variable its choices;
- Add another rule, joined by All of them (
&&) or Any of them (||); - Add an else branch, for the text to use when the condition doesn't hold.
The panel shows the exact tag it will write, then wraps your selection with each tag on its own line. It refuses a selection that would cut a {variable} or a tag in half, or take part of an existing section. The variables offered are the ones that can exist where the section is decided. In a flow's shared prompt, none of the step-written variables are offered, since the shared prompt is decided before any step runs. In a step, it's the variables that can exist when the call enters that step.
Preview with values
Preview with values is under the single-context system prompt and the flow's shared prompt. Type sample values (Yes / no and Choice variables are dropdowns; leave a field blank to preview a call that didn't send it) and the panel shows:
- Sections: what each section decided, e.g. "Section on line 3: took {#elif} #1 on line 5 (branch 2 of 3)" or "dropped — no condition held and it has no {#else}";
- errors and warnings for the prompt, and warnings for the greeting;
- What the model receives: the prompt exactly as the model gets it for those values.
The preview is rendered by the server with the same code a live call runs, including declared defaults and typed-value rewriting, so it can't disagree with a real call. Nothing is worked out in your browser and no model is called. The platform's own delivery, voice and identity instructions are added separately and are not shown.
Flow agents
- Every step's problems are in the validation panel: one row per section problem in every step prompt, each with a jump to its step, so a broken step you haven't opened still shows. A problem the published flow already has is listed as a warning and doesn't block publishing.
- A step's effective prompt preview labels its two parts when sections are involved: "Agent brief · decided once, when the call starts" and "This step · decided when the call reaches this step".
- The Variables panel checks types against every condition in the agent: the shared prompt, step prompts and equation exits.
Generating prompts with AI
- The prompt generators (Generate this prompt from a brief, the flow's global-prompt and step-instruction generators, and Improve the current prompt) can write sections when the agent has declared variables and your brief describes behaviour that depends on them ("if they're a returning customer, skip the plan overview"). They use only the agent's declared variables, and write nothing conditional for an agent that has none. A generated prompt whose tags don't pair up, or that uses another template syntax, is flagged in the generator's warnings before you apply it.
- TeleNow FDE, the in-product AI assistant, can write sections and fix broken ones. Its changes are saved through the same check as the builder's, so it can't store a section that doesn't pair up.
Both test only variables the agent actually has: its declared variables, plus (when writing by hand or with the FDE) the built-in ones and, in a step's prompt, a variable an earlier step writes. A name nothing supplies is always empty, so a condition on it is decided the same way on every call.
Where sections are not supported
Sections work only in prompts the model reads. Anything spoken or sent word for word is used exactly as written, so a tag there would be read aloud to the caller or sent as text:
- greetings and openers (inbound and outbound), and a
firstResponsepassed with a call; - a step's spoken lines: the line spoken on arrival, a Say step's line, a keypad menu, Play Audio's Say something first, an Ending's closing line, wait lines, a transfer's lines, and a Function step's "one moment" line;
- a Function (
tool) step's prompt, which is a note for you that never reaches the model, and Note steps; - post-call email and WhatsApp messages, and connector request templates.
The builder flags a tag in greetings, openers, arrival lines, Say and menu lines, Play Audio's intro and closing lines. POST /api/agents/flow/validate also flags arrival and wait lines (section_in_spoken_line). To vary what is said, put the condition in the prompt ("If the caller is a returning customer, open with…"), or in a flow, route to a different Say step with an exit.
Common mistakes
Each of these is an error, which blocks Publish in the builder and refuses the save over the API, unless it is marked as a warning (or, for crossed sections, as neither).
- Handlebars
{{#if x}}…{{/if}}— warningsection_near_miss.{{is the literal-brace escape, so this reaches the model as text. Write{#if {x} == …}and{/if}. - Jinja or Liquid
{% if %}…{% endif %}— warningsection_near_miss, with the tag it should be. - Typos and guesses:
{#endif},{/fi},{# if …},{if …},{else},{#else if …}, full-width{ }— warningsection_near_miss, e.g. "Did you mean{/if}?" (A declared variable actually namediforelseis not reported.) - No comparison:
{#if {vip}}—section_no_operator. Write{vip} == true, or{vip} != ''for "has a value". - Curly quotes:
{#if {plan} == ‘trial’}—section_smart_quotes. Retype them as straight'or". - A single
=(or===,=>,<>) —section_bad_operator, with the operator to use (==,>=,!=). and,or,not—section_word_operator. Use&&,||and!=. To compare with one of these words as text, quote it.- Parentheses —
section_parentheses. Nest sections instead. !{x}—section_bang_operator. Use{x} == ''or{x} != 'value'.- Chained comparisons:
18 < {age} < 65—section_bad_operator. Write{age} > 18 && {age} < 65. - A single
&or|—section_bad_operator. Use&&or||, and quote a value that contains the character. - Unmatched quotes or braces, or a filter in a condition (
{name|lower}) —section_bad_condition. {#if }with nothing in it —section_empty_condition.- A missing
{/if}—section_unclosed, reported on the innermost{#if}that was never closed. - An extra
{/if}—section_stray_close. {#elif}or{#else}outside a section —section_stray_branch.{#elif}after{#else}—section_elif_after_else. Two{#else}—section_double_else.- A tag split over two lines —
section_unterminated_tag. - Sections that cross, like
{#if A}…{#if B}… a{/if}meant for A … a{/if}meant for B — no error and no warning, because{/if}always closes the most recent open section, so B ends where you meant A to end. Nest sections fully, one inside the other. Add condition refuses a selection that takes part of an existing section. !=on an optional variable with no default — warningsection_ne_on_optional: the branch is taken whenever the variable is missing (why).- A typed variable compared with a value it never holds (
{vip} == 1on a Yes / no,{plan} == 'gold'when the choice isGold,{age} == 'ten'on a Number) — warningsection_literal_vs_type, naming the value to use. - A variable nobody supplies (a typo such as
{Plan}for{plan}) — warningsection_unknown_var, with "Did you mean{plan}?". - A section that names an attached tool — warning
section_names_tool(why).
Every code
Errors. These are structure problems. The save is refused, and if a stored prompt somehow has one, the call uses that prompt with the tags left in as text.
| Code | What is wrong |
|---|---|
section_unclosed | An {#if} has no {/if} before the end of the field |
section_stray_close | A {/if} has no open {#if} |
section_stray_branch | An {#elif} or {#else} is outside any section |
section_elif_after_else | An {#elif} comes after the section's {#else} |
section_double_else | A section has two {#else} |
section_unterminated_tag | A tag doesn't close with } on its own line |
section_empty_condition | {#if } or {#elif } has no condition |
section_no_operator | A condition compares nothing |
section_smart_quotes | A condition uses curly or full-width quotes |
section_bad_operator | =, ===, =>, a lone ampersand or a lone pipe character (write && or the double-pipe or operator), or more than one comparison in one pair |
section_word_operator | and, or or not used as an operator |
section_parentheses | Parentheses in a condition |
section_bang_operator | ! before a variable |
section_bad_condition | Unmatched quotes or braces, a side missing, or a {name} that isn't a plain variable name |
Warnings. The prompt is well-formed and saves, but it may not do what you meant.
| Code | When |
|---|---|
section_near_miss | Text that looks like a tag but isn't one (other template syntaxes, typos) |
section_in_spoken_line | A tag in a greeting or a spoken step line |
section_unknown_var | A condition reads a variable that isn't declared, built in, or written by any step |
section_reads_runtime_var | Single-context: a condition reads a variable only written during the call |
section_base_reads_captured | Flow shared prompt: a condition reads a variable only a step writes |
section_ne_on_optional | != on an optional variable with no default |
section_literal_vs_type | A typed variable compared with a value it never holds |
section_ordering_on_text | > / < on a variable that holds text (a non-number type, or a date/time variable) |
section_mixed_and_or | One condition mixes && (and) with the double-pipe or operator — && is evaluated first |
section_empty_possible | All of the prompt is inside sections and some values keep none of it |
section_duplicate_condition | An {#elif} repeats an earlier condition of the same section |
section_contradictory | A nested condition can never hold given the branch it sits in |
section_names_tool | A section names an attached tool |
missing_braces | A condition uses a variable's name without braces |
Limits
- A prompt that comes out empty is used as written. If every section is skipped and nothing is left, the call doesn't run with no instructions. It uses the prompt exactly as written, tags and every branch included, and logs it. The builder warns when this can happen (
section_empty_possible): keep the core instructions outside any section, or give the sections an{#else}. - Sections change words, not tools. A tool attached to the agent or step stays available when the section that mentions it is skipped, and the model can still call it. "Only issue refunds for Pro plans" inside a skipped section removes the sentence, not the refund tool.
- Post-call analysis sees the branch the call ran. The judge that checks the agent's answers against its prompt reads the prompt as this call got it, not the branches it skipped. Each call also records which branch every section took (the section and branch only, never the values).
- Estimates count every branch. Estimates that read the prompt text (the builder's context-size warning, the AI assistants' cost estimates) count every branch, so for an agent that uses sections they are an upper bound.
- Prompts without tags are unaffected. A prompt with no section tag renders exactly as it did before sections existed.
Tip: put conditional sections after the fixed instructions. Model providers cache the start of a prompt that repeats from call to call, and text that varies near the top makes those repeats shorter.
Over the API
- Save:
POST /api/agentsandPUT /api/agents/{id}checksystemPromptand every step prompt inmetadata.flow, each on its own, and refuse a structure error with400andcode: "prompt_sections_malformed", listing every problem. Drafts (metadata.flowDraft,metadata.configDraft), greetings, spoken lines, andtoolandnotestep prompts are not checked. An update is refused only for a problem the stored agent doesn't already have. See Agents API → Errors. - Check and preview:
POST /api/agents/prompt/validatereturns the same errors and warnings for a single-context or shared prompt and, given samplevalues, the prompt exactly as a call would get it, with each section's decision. See Agents API → Check a prompt. - Step prompts:
POST /api/agents/flow/validatereports section problems in every step prompt and spoken line as warnings on that step (Flow agents API). - Types:
metadata.variables[].typeandchoices(field reference).
FAQ
Does this change anything for agents that don't use sections?
No. A prompt with no section tag is rendered exactly as before, byte for byte.
Can a section depend on something the caller says during the call?
Not in a single-context agent: its sections are decided at call start. In a flow agent, capture the value in one step and put the section in the prompt of a step that comes after it. Or route on it with an exit.
Can I branch on the time or the day?
On the day, yes: {current_weekday} == 'Sunday' (the exact format of the built-in variable). Not with > or < on {current_time}, which is text like 06:15 PM, so the comparison is always false. Either way the section is decided when the call starts, so a call that runs past 6 pm keeps its branch.
Can I branch on the caller's language?
Only on a value supplied when the call starts (say, {language} from your CRM), and it won't follow a switch mid-call. Use the agent's language settings for that.
Why does my agent read the tags out loud?
Either the tag is in a greeting or a spoken line, where sections aren't supported, or the prompt has a structure problem saved before this check existed, so the call uses it with the tags as text. Open the agent: the problems list shows the cause.
How do I write a literal {#if in a prompt?
Write {{#if. The model sees {#if.
Does the preview cost anything?
No. It renders the prompt on the server without calling a model.
See also
- Context variables: declaring variables, required flags, defaults, and how values reach a call.
- Flow agents (graph): sections in step prompts.
- Building agents: the Context step.
- Flow agents API → Equations: the same condition grammar on exits.