L4 Routing Rules
Answer
Routing rules are the deterministic half of L4: `goto` jumps to a real step, `say` speaks a pre-cached phrase at zero latency, `say` with return_to_cursor reassures then resumes, and `say` with end_call closes politely. Global rules apply at every step; step-level rules apply only where you set them.
Audience: workflow authors + reviewers. How to write IF/THEN rules that control what the agent does when a customer says something off the happy path.
TL;DR cheat sheet
goto = jump to a real step in the graph (it has its own prompt, niti rules, sub-flow).
say = speak THIS exact phrase, pre-cached as TTS, deterministic. 0 ms latency.
say + return_to_cursor: true = reassure / explain → resume where customer was.
say + end_call: true = polite close → terminate gracefully.
Workflow-level (Global) = applies at every step (cancel, fraud, interest, off-topic).
Step-level = applies only here (loan-type pick, salary hesitation, branch-specific).
say is pre-built into the TTS cache when you click Apply. Editing the text re-builds on next Apply.
Quick-decide examples
| Customer says… | Use | Why |
|---|---|---|
| "ये fraud तो नहीं?" | say + return_to_cursor (Global) | One-line reassurance, then continue the flow. Can happen at any step. |
| "Interest कितना लगेगा?" | say + return_to_cursor (Global) | Brief explainer, return to where they were. Universal question. |
| "रहने दीजिए" (cancel) | say + end_call (Global) | Polite goodbye + terminate. Universal. |
| "Personal loan" on collect_loan_type | goto → personal_company_name (Step) | Whole sub-flow follows — branching into another collection step. |
| "Home loan" on collect_loan_type | goto → home_property_type (Step) | Same — full sub-flow. |
| "मुझे appointment book करना है" | say + end_call (Global) | Off-topic — say "we don't handle that" + close. |
| Hesitates to share salary | say + return_to_cursor (Step on collect_salary) | Confidentiality reassurance + re-ask the same step. |
| "कौन सा बेहतर है?" on loan-type pick | say + return_to_cursor (Step on collect_loan_type) | Explain options briefly + re-ask the same question. |
L4 Prompt vs L4 Routing Rules — the big distinction
Both shape what L4 BUDDHI does at a step, but they answer different questions. Mixing them up is the #1 authoring mistake (the 2026-06-09 + 2026-06-10 Cab Booking traces both had routing intent described in the L4 prompt textarea, where L4 ignores it).
| Aspect | L4 Prompt step.l4.prompt | L4 Routing Rule l4_routing_rules |
|---|---|---|
| Question it answers | HOW should L4 reason here? | WHERE should L4 go when customer says X? |
| Shape | Free-text directive | Structured {when, goto|say, flags} |
| Controls exact wording? | No — L4 still generates each turn | Yes (with say) — exact pre-cached TTS |
| Can navigate to other steps? | No | Yes — goto: step_id |
| Can end the call? | No | Yes — end_call: true |
| Latency | L4 LLM call (~2-4 sec) | say = 0 ms cached; goto = also 0 ms |
| Cost | ~₹1 per turn (LLM) | ₹0 when rule fires |
Mental model
L4 prompt = the agent's personality + rules for this step (tone, what to validate, what to never do).
Routing rules = shortcuts — pre-written decisions for specific customer intents that skip reasoning entirely.
6 worked examples — which goes where?
1. Reject single-digit salary answers
Need On the salary-collect step, accept "5 lakh" but re-ask on bare "5".
Use L4 Prompt.
Why Validation behavior — L4 still reasons each turn, but with a stricter rule.
step.l4.prompt:
"On THIS step, accept salary only if customer mentions a unit
(lakh / thousand / k / per month). If they reply with just a bare
number, briefly explain you need the monthly amount in rupees,
then re-ask."2. Fraud reassurance — exact wording, anywhere in the call
Need Customer asks "ये fraud तो नहीं?" at any step → reply with one EXACT line, then continue.
Use Routing Rule (workflow-level, say + return_to_cursor).
Why Universal intent + exact wording required + must resume current step.
{
"when": "customer doubts the call",
"say": "बिल्कुल सही सवाल है। हम कोई OTP नहीं माँगते — सिर्फ़ basic details चाहिए।",
"return_to_cursor": true
}3. Loan-type pick — branch into sub-flow
Need On loan-type step, "Personal loan" → jump to personal-loan sub-flow.
Use Routing Rule (step-level, goto).
Why Branching into a multi-step sub-flow with its own collection logic.
{
"when": "customer says Personal loan",
"goto": "personal_company_name"
}4. Stop English drift
Need L4 keeps switching to English on Hindi-speaking customers.
Use L4 Prompt.
Why Style/persona constraint — applies to every L4 turn, not a specific intent.
step.l4.prompt:
"ALWAYS reply in Hindi using Devanagari script even if customer
mixes English words. Never switch to pure English."5. Polite cancel — end gracefully
Need Customer says "रहने दीजिए" / "cancel कर दो" → one polite line + terminate.
Use Routing Rule (workflow-level, say + end_call).
Why Universal intent + exact wording + must terminate.
{
"when": "customer wants to cancel mid-flow",
"say": "कोई बात नहीं, जब चाहें call कर दीजिए। धन्यवाद!",
"end_call": true
}6. Never accept "skip" on a required slot
Need On the date-collect step, never let customer bypass with "skip" / "baad mein".
Use L4 Prompt.
Why Step-specific validation rule. Not navigation — just a what-NOT-to-do directive.
step.l4.prompt:
"On THIS step, NEVER accept 'skip', 'pass', 'baad mein', or any
deferral. The date is required for booking. If customer tries to
skip, explain we can't proceed without it and re-ask."Decision flow (when in doubt)
1. Does the customer's utterance match a SPECIFIC intent you can name?
NO → it's a general behavior-shaping problem → L4 Prompt
YES → ↓
2. Should the next agent line be ONE EXACT pre-written phrase?
NO → multi-step branch follows → Routing Rule with `goto`
YES → ↓
3. After saying it, where should the cursor go?
Same step (resume) → `say` + return_to_cursor
End the call → `say` + end_call
Next step (advance) → `say` only (rare)Common mistakes — anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Routing logic written in the L4 prompt: "If customer says cancel, end the call" | L4 won't act on it — descriptive, not actionable | Routing rule with end_call: true |
| Persona/tone in routing rules | No field for it | Put it in step.l4.prompt |
| say for templated text with {customer_name} | say is pre-cached at Apply time — no runtime interpolation | goto a real speak step that supports templating |
| Both goto AND say set on the same rule | Runtime prefers say; goto is silently ignored | Pick one per rule |
| Step-specific routing on the workflow panel | Fires on every step (weird jumps from unrelated steps) | Move to the specific step's routing panel |
The four places L4 reasoning lives
| # | Where | Field | Applies | Example |
|---|---|---|---|---|
| A | Workflow routing rule | wf.global_l4_routing_rules | Every step | Cancel mid-flow, fraud doubt, interest rate question |
| B | Step routing rule | step.l4_routing_rules | This step only | "personal loan" → personal_company_name |
| C | Step L4 prompt | step.l4.prompt | This step's L4 reasoning style | "Be empathetic; never accept a range, only a single number" |
| D | say text on a rule | rule.say + flags | Replaces goto for canned responses | "हम कोई OTP नहीं माँगते…" |
D is a property of a rule, not a separate storage location. A single rule can use either goto (jump) or say (canned response).
Rule schema — the two flavours
Flavour 1 — goto (jump to a real step)
{
"when": "customer says personal loan",
"goto": "personal_company_name"
}Use when the next thing isn't just "say one line" — it's a whole sub-flow with its own collection logic, branches, sub-steps.
Flavour 2 — say (canned response, pre-cached)
{
"when": "customer doubts the call",
"say": "बिल्कुल सही सवाल है। हम कोई OTP या payment नहीं माँगते — सिर्फ़ eligibility check के लिए basic details चाहिए।",
"return_to_cursor": true
}Use when you want to say one specific phrase and then resume the conversation where the customer was. Pre-cached at apply time, 0 ms latency, identical wording every call.
Flag combinations on a say rule
| Flag combo | Behaviour |
|---|---|
| say + return_to_cursor: true | Speak the phrase, then resume current step (customer will continue answering the original question) |
| say + end_call: true | Speak the phrase, then terminate the call gracefully |
| say only (no flags) | Speak the phrase, advance flow normally (rare — usually you want one of the above) |
The decision rule (four-way)
For each rule, ask in order:
-
"Could the customer say this at MORE THAN ONE step?"
Yes → workflow-level (A)
No → step-level (B) -
"Is the response a one-line reaction or a multi-step branch?"
One-liner / single sentence →say(D)
Multi-step (own collection, own branches) →goto -
"After speaking, where should the cursor go?"
Back to the same step where customer was →return_to_cursor: true
End the call →end_call: true
Advance normally to next step → leave both off (rare) -
"Is this about style/persona, not navigation?"
Yes → it belongs instep.l4.prompt, not a routing rule.
Example: "On THIS step, never accept a salary range — only a single number"
Complete example — Loan Eligibility (copy-pasteable)
STEP 1: Opening
IF customer agrees to proceed (e.g., "हाँ बताइए"):
JUMP TO: "collect_loan_type"
IF customer declines (e.g., "नहीं चाहिए", "busy हूँ"):
JUMP TO: "धन्यवाद, future में जब चाहें call करें। आपका दिन शुभ हो!"
STEP 2: Collect Loan Type
IF customer says "Personal loan":
JUMP TO: "personal_company_name"
IF customer says "Home loan":
JUMP TO: "home_property_type"
IF customer says "Car loan":
JUMP TO: "car_model"
IF customer says "Business loan":
JUMP TO: "business_sector"
IF customer is confused (e.g., "कौन सा बेहतर है?"):
JUMP TO: "कोई बात नहीं — personal अपने ख़र्च के लिए, home घर ख़रीदने के लिए, car गाड़ी के लिए, business अपने काम बढ़ाने के लिए। बताइए कौन सा उपयुक्त लगेगा?"
STEP 3A: Personal salary
IF customer hesitates to share salary (e.g., "ये बताना ज़रूरी है?"):
JUMP TO: "जी हाँ, ये सिर्फ़ eligibility check के लिए है। आपकी जानकारी पूरी तरह गोपनीय रहेगी।"
GLOBAL EXCEPTIONS
IF customer wants to cancel mid-flow (e.g., "रहने दीजिए"):
JUMP TO: "कोई बात नहीं, जब भी सुविधाजनक हो हमें call कर दीजिए। धन्यवाद!"
IF customer doubts the call (e.g., "ये fraud तो नहीं?"):
JUMP TO: "बिल्कुल सही सवाल है। हम कोई OTP या payment नहीं माँगते — सिर्फ़ eligibility check के लिए basic details चाहिए।"
IF customer asks about interest rate (e.g., "Interest कितना लगेगा?"):
JUMP TO: "जी, interest rate आपकी profile और loan amount पर निर्भर करता है। हमारा loan officer call पर सटीक details देगा।"How the parser interprets it
| JUMP TO content | Treated as | Why |
|---|---|---|
| "collect_loan_type" | goto: "collect_loan_type" | Matches an existing step_id |
| "personal_company_name" | goto: "personal_company_name" | Matches existing step |
| "धन्यवाद, future में... शुभ हो!" | say + end_call: true | Long sentence + closing phrase ("शुभ हो") auto-detected |
| "कोई बात नहीं — personal..." | say + return_to_cursor: true | Long sentence, no closing marker — default |
| "बिल्कुल सही सवाल है..." | say + return_to_cursor: true | Long sentence — canned reassurance |
| "जी हाँ, ये सिर्फ़..." | say + return_to_cursor: true | Long sentence — confidentiality explainer |
How the pre-caching works
When you click Apply on the Designer panel:
- The parser splits the script into global + per-step buckets and writes them to the workflow.
- For every rule with a
sayfield, the runtime:
Computes a stableintent_key:routing_global_<idx>__<sha>orrouting_step_<sid>_<idx>__<sha>
Calls the workflow's TTS provider (Sarvam / ElevenLabs / etc.) with the workflow's voice + language
Stores the resulting clip as a sample withstep_role: "routing_response" - The Designer's response shows
precache: {jobs_total: N, synthesised: N}so you can confirm.
At runtime — what L4 sees
GLOBAL ROUTING HINTS (apply at ANY step):
- when "customer wants to cancel mid-flow" → action:play_cache, intent_key:routing_global_0__a1b2c3d4ef
(canned: "कोई बात नहीं, जब भी सुविधाजनक..."), end_call:true
- when "customer doubts the call" → action:play_cache, intent_key:routing_global_1__f5e4d3c2b1
(canned: "बिल्कुल सही सवाल है..."), return_to_cursor:true
- when "customer asks about interest rate" → action:play_cache, intent_key:routing_global_2__9876543210
(canned: "जी, interest rate आपकी..."), return_to_cursor:true
STEP-SPECIFIC ROUTING HINTS (for THIS step):
- when "customer says Personal loan" → goto_step:personal_company_name
- when "customer says Home loan" → goto_step:home_property_type
- when "customer is confused" → action:play_cache, intent_key:routing_step_collect_loan_type_4__abcdef0123
(canned: "कोई बात नहीं — personal..."), return_to_cursor:true
Hints are advisory — apply only if the customer's utterance actually matches the 'when' description.Override pattern (rare but important)
When a global rule needs a DIFFERENT target on a specific step.
Example: globally, "ask about interest rate" → generic explainer.
But on collect_loan_type, asking about interest rate is suspicious
(no loan type picked yet) — redirect back into the funnel.
GLOBAL EXCEPTIONS
IF customer asks about interest rate:
JUMP TO: "जी, interest rate आपकी profile पर निर्भर करता है। हमारा loan officer call पर बताएगा।"
STEP 2: Collect Loan Type
IF customer asks about interest rate: ← same `when`, overrides global
JUMP TO: "पहले loan type चुन लीजिए — फिर interest rate के बारे में बता दूँगी।"
The step-level rule wins on label collision (case + whitespace insensitive). L4's prompt for
collect_loan_type shows only the step-level version. Other steps see the global rule normally.
Where does this rule belong? — full cheat table
| Customer intent / question | Layer | Rule type | Why |
|---|---|---|---|
| "मुझे cancel करना है" (cancel mid-flow) | Workflow | say + end_call | Can happen at any step; one-line close |
| "Interest कितना है?" | Workflow | say + return_to_cursor | Universal question; brief explainer |
| "ये fraud तो नहीं?" | Workflow | say + return_to_cursor | Universal anxiety; reassurance |
| "कौन बोल रहा है?" (who's calling) | Workflow | say + return_to_cursor | Brief identity reminder |
| 5+ seconds of silence | Workflow | say + return_to_cursor | Check if customer is still there |
| "Personal loan" on collect_loan_type | Step | goto | Only meaningful here; whole sub-flow |
| "Salary बताना ज़रूरी है?" on collect_salary | Step | say + return_to_cursor | Slot-specific objection; one-line reassurance |
| "नहीं decide किया कौन सी car" on car_model | Step | say + return_to_cursor | Branch-specific hesitation; brief explainer |
| "Same number use करो" on phone_collect | Step | say + return_to_cursor | Slot-specific request; confirm + re-ask |
| "Be more empathetic" general note | step.l4.prompt | (style) | Persona/style directive, not routing |
| "Never accept a range — only a number" | step.l4.prompt | (style) | Validation behavior, not routing |
Common mistakes
- Putting universal rules per-step — copying "interest rate question" onto every step's rules will drift. Edit on step 1, forget on step 7. Use workflow-level.
- Putting step-specific rules at workflow level —
"Personal loan"at workflow level would fire on the salary step too, causing weird jumps. Use step-level. - Using L4 prompt for routing decisions — the L4 prompt is for how to talk, not where to go. Use routing rules for navigation.
- Missing the
gototarget — make sure the target step exists in the workflow. The parser surfaces unmatched ones in "⚠ Unmatched headings" — fix those before testing. - Vague
whenclauses —"customer is unhappy"→ too vague, L4 won't classify reliably."customer expresses frustration about long wait time"→ much sharper. Be specific. - Forgetting
return_to_cursoron canned responses — without it, the agent says the canned line and then the cursor advances. Most reassurance / explainer flows wantreturn_to_cursor: true. - Editing
saytext but not clicking Apply — the rule field saves on edit, but the new audio isn't synthesised until Apply runs. Stale audio plays until then. - Mixing
end_callandreturn_to_cursor— contradictory. Designer clears one when the other is set; parser dropsreturn_to_cursorwhen it auto-detects an end_call closing phrase like "शुभ हो" / "have a good day".
Performance + cost reference
| Path | Latency at runtime | Cost per turn | Wording variance |
|---|---|---|---|
| goto to a real speak step (pre-cached audio) | 0 ms TTS | ₹0 | Identical ✅ |
| say rule (pre-cached at apply time) | 0 ms TTS | ₹0 | Identical ✅ |
| L4 falls back to generate (no matching rule) | ~4–6 sec | ~₹1 | Varies every call ⚠ |
The say field is what makes "I want the agent to say EXACTLY this" achievable without bloating the step graph with one-line reaction steps.
Appendix — Pure JSON examples
For reviewers reading the parsed result rather than the IF/THEN script.
Workflow-level rules (wf.global_l4_routing_rules)
[
{
"when": "customer wants to cancel mid-flow",
"say": "कोई बात नहीं, जब भी सुविधाजनक हो हमें call कर दीजिए। धन्यवाद!",
"end_call": true
},
{
"when": "customer doubts the call",
"say": "बिल्कुल सही सवाल है। हम कोई OTP या payment नहीं माँगते — सिर्फ़ eligibility check के लिए basic details चाहिए।",
"return_to_cursor": true
},
{
"when": "customer asks about interest rate",
"say": "जी, interest rate आपकी profile और loan amount पर निर्भर करता है। हमारा loan officer call पर सटीक details देगा।",
"return_to_cursor": true
}
]Step-level rules on collect_loan_type (step.l4_routing_rules)
[
{ "when": "customer says Personal loan", "goto": "personal_company_name" },
{ "when": "customer says Home loan", "goto": "home_property_type" },
{ "when": "customer says Car loan", "goto": "car_model" },
{ "when": "customer says Business loan", "goto": "business_sector" },
{
"when": "customer is confused about loan type options",
"say": "कोई बात नहीं — personal अपने ख़र्च के लिए, home घर ख़रीदने के लिए, car गाड़ी के लिए, business अपने काम बढ़ाने के लिए। बताइए कौन सा उपयुक्त लगेगा?",
"return_to_cursor": true
}
]Last reviewed