# L4 Routing Rules

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.

```text
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.

```json
{
  "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.

```json
{
  "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.

```text
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.

```json
{
  "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.

```text
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)

```text
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)

```json
{
  "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)

```json
{
  "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) |

> return_to_cursor and end_call are mutually exclusive. The Designer UI enforces this — checking end_call automatically clears return_to_cursor.

## The decision rule (four-way)

For each rule, ask in order:

1. 
          **"Could the customer say this at MORE THAN ONE step?"**
          
Yes → workflow-level (A)
No → step-level (B)
        
2. 
          **"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`
        
3. 
          **"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)
        
4. 
          **"Is this about style/persona, not navigation?"**
          
Yes → it belongs in `step.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)

```text
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:

1. The parser splits the script into global + per-step buckets and writes them to the workflow.
2. For every rule with a `say` field, the runtime:
          
Computes a stable `intent_key`: `routing_global_<idx>__<sha>` or `routing_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 with `step_role: "routing_response"`
        
3. The Designer's response shows `precache: {jobs_total: N, synthesised: N}` so you can confirm.

> Edit a rule's say text → new clip on next Apply. The old one is garbage-collected by the audio-prune sweep. No manual cache-bust needed.

### At runtime — what L4 sees

```text
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.
      

```text
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

1. **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.**
2. **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.**
3. **Using L4 prompt for routing decisions** — the L4 prompt is for *how* to talk, not *where* to go. **Use routing rules for navigation.**
4. **Missing the ****`goto`**** target** — make sure the target step exists in the workflow. The parser surfaces unmatched ones in "⚠ Unmatched headings" — fix those before testing.
5. **Vague ****`when`**** clauses** — `"customer is unhappy"` → too vague, L4 won't classify reliably. `"customer expresses frustration about long wait time"` → much sharper. **Be specific.**
6. **Forgetting ****`return_to_cursor`**** on canned responses** — without it, the agent says the canned line and then the cursor advances. **Most reassurance / explainer flows want ****`return_to_cursor: true`****.**
7. **Editing ****`say`**** text 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.
8. **Mixing ****`end_call`**** and ****`return_to_cursor`** — contradictory. Designer clears one when the other is set; parser drops `return_to_cursor` when 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`)

```json
[
  {
    "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`)

```json
[
  { "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
  }
]
```