> Markdown rendition of https://typeglish.dev/blog/escalate-words-vs-tool ("Bake-off: say escalate vs. call the escalation tool - Tidelines"). Canonical page: https://typeglish.dev/blog/escalate-words-vs-tool · All pages: https://typeglish.dev/llms.txt

[← Tidelines](https://typeglish.dev/blog)/ Best practices Sep 23, 2026

# Bake-off: say escalate vs. call the escalation tool

Handing a conversation to a human is the one behaviour every support agent has. There are two ways to write it down, they score identically, and each one catches the accident the other sleeps through.

by **TypeGlish team** 10 min read #best-practices

Two ways out of the conversation.

**TL;DR** Write the handover twice: `MUST escalate to a human agent.` puts the act on the logic plane, where a later `NEVER hand off to a human agent.` is a proven contradiction, and `MUST call @[transfer_to_human].` puts the mechanism on the structure plane, where a renamed tool is a refused build. The two prompts tie at `A (99/100)` on every number the toolchain prints, so the choice is decided entirely by which edit lands on you first.

Lumen Fibre runs a chat agent in front of its broadband support queue. Two rules move a conversation to a person: the customer asks for one, or the customer mentions legal action. Both are uncontroversial, both are in the launch prompt, and the team is arguing about how to write them. One camp says an instruction in plain English. The other says the model should call the transfer tool and the prompt should say so. Here are both files, and the checker as referee.

## §1 Two routes, one handover

Route A writes the act. *Escalate* is a verb in the routing family, so the checker has a frame for it and reads the line as a rule.

words.tg - the handover as an instruction ✓ A (99/100)

```typeglish
# Role
@@ role: one named agent on one desk, so every later "you" is this agent
You are Wren, a support agent for Lumen Fibre, a broadband provider.

# Constraints
@@ ask: a customer who asks for a person gets a person
- WHEN the customer asks for a human THEN MUST escalate to a human agent.
@@ legal: a threat of legal action stops being a support conversation
- WHEN the customer mentions legal action THEN MUST escalate to a human agent.
@@ brevity: a handover line is read on a phone
- MUST keep every reply to at most 3 sentences.

$TEST asks_for_a_person
  - input:: I do not want to do this with a bot. Put me through to someone.
  - expect::
    - escalates to a human agent
    - at most 3 sentences
```

Nothing in this file knows how a handover happens. That is either the point or the problem, depending on which camp you are in.

Route B declares the tool and points at it. `@[name]` is a tool pointer: the compiler resolves it against a declared `$TOOL`, and the artifact carries the bare tool name while the schema ships in the bundle.

tool.tg - the handover as a tool call ✓ A (99/100)

```typeglish
$TOOL transfer_to_human
  - description:: Hand the conversation to a human agent in the support queue.
  - input::
    - reason:: string

# Role
@@ role: one named agent on one desk, so every later "you" is this agent
You are Wren, a support agent for Lumen Fibre, a broadband provider.

# Constraints
@@ ask: a customer who asks for a person gets a person
- WHEN the customer asks for a human THEN MUST call @[transfer_to_human].
@@ legal: a threat of legal action stops being a support conversation
- WHEN the customer mentions legal action THEN MUST call @[transfer_to_human].
@@ brevity: a handover line is read on a phone
- MUST keep every reply to at most 3 sentences.

$TEST asks_for_a_person
  - input:: I do not want to do this with a bot. Put me through to someone.
  - expect::
    - calls transfer_to_human
    - at most 3 sentences
```

Same two triggers, same reply bound, same test case. The only difference is what sits after `THEN MUST`.

## §2 The referee calls it a tie

Not a narrow one. Identical grade, identical planes, identical facets, identical rule counts.

tg check and tg score on both routes - output

```
$ npx typeglish check words.tg tool.tg
✓ 2 files — 0 error, 0 warning, 0 info

$ npx typeglish score words.tg
words.tg — A (99/100)  proven errors: none  tiers: base+z3
  planes  runtime 99 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 93 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   3 of 3 rule-shaped lines read · 3/3 covered by $TEST

$ npx typeglish score tool.tg
tool.tg — A (99/100)  proven errors: none  tiers: base+z3
  planes  runtime 99 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 93 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   3 of 3 rule-shaped lines read · 3/3 covered by $TEST
```

A tool call is a rule. `call` is a light verb in the execution family, semantically bleached, and the tool object carries the meaning, so `MUST call @[transfer_to_human].` earns exactly the enforceability credit that `MUST escalate to a human agent.` does.

The test suites tie too, including the part people assume will separate them. *calls transfer_to_human* is not a deterministic assert. It is a prose expectation, graded by a judge on a live run, exactly like *escalates to a human agent*.

tg test --dry on both suites - output

```
$ npx typeglish test words.tg tool.tg --dry
✓ words.tg  coverage: 3/3 rules exercised
  · asks_for_a_person — "I do not want to do this with a bot. Put me through to someo" (not run)
      ✓ at most 3 sentences
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
✓ tool.tg  coverage: 3/3 rules exercised
  · asks_for_a_person — "I do not want to do this with a bot. Put me through to someo" (not run)
      ✓ at most 3 sentences
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 2 prompts — 0 failed
```

Only the length assert runs offline in either file. If you want the handover itself checked without a model in the loop, neither route gives it to you: see [deterministic asserts vs. an LLM judge](https://typeglish.dev/blog/test-asserts-vs-judge) for what does.

So the tie stands everywhere a report can see it. What separates the two files is not a number today. It is an edit next month, and there are two of them.

## §3 The first edit: the platform renames the tool

This one is dull and it happens constantly. The platform team folds the support queue into a shared routing service and renames `transfer_to_human` to `transfer_to_agent`. Somebody updates the declaration and the first rule, and misses the second.

tool-renamed.tg - the rename, with one reference left behind ✗ 1 error

```
$TOOL transfer_to_agent
  - description:: Hand the conversation to a human agent in the support queue.
...
- WHEN the customer asks for a human THEN MUST call @[transfer_to_agent].
- WHEN the customer mentions legal action THEN MUST call @[transfer_to_human].
```

The legal rule still points at the old name. In a prompt file, as in any other text document, nothing about this looks wrong.

the rename, checked and built, then the same day in words.tg - output

```
$ npx typeglish check tool-renamed.tg
tool-renamed.tg:14:58  error  structure/undefined-tool-inline  @[transfer_to_human] — no $TOOL or $IMPORT tool named "transfer_to_human" in this file. Define it ("$TOOL transfer_to_human") or import it ("$IMPORT tool transfer_to_human") so the pointer has a target.

✗ 1 file — 1 error, 0 warning, 0 info

$ npx typeglish build tool-renamed.tg --out-dir out
typeglish build: tool-renamed.tg refused — nothing written
tool-renamed.tg:14:58  error  structure/undefined-tool-inline  @[transfer_to_human] — no $TOOL or $IMPORT tool named "transfer_to_human" in this file. Define it ("$TOOL transfer_to_human") or import it ("$IMPORT tool transfer_to_human") so the pointer has a target.

✗ 1 file — 1 error, 0 warning, 0 info

$ npx typeglish build words.tg --out-dir out
✓ built out/words.txt ← words.tg (d932469b3972, full)
```

The pointer route names the line and the column and refuses to write anything. The words route has no name to get wrong, so the rename is not an edit it can notice: the file is untouched, still `A (99/100)`, and it still builds an artifact that instructs an act the runtime can no longer perform.

Point one to Route B, and it is a bigger point than it looks. A pointer is the only part of a prompt that is checked against something outside the sentence it sits in. Everything else in a `.tg` file is proven against other sentences in the same file.

## §4 The second edit: somebody bans the handover

Now the other kind of Tuesday. The queue closes at eight, the out-of-hours experiment starts, and someone in operations adds two lines to the prompt. They write them the way people write policy, not the way the file happens to be written.

the two lines appended to each file, byte-identical

```
@@ hours: the queue closes at 8pm and there is nobody there to take it
- NEVER hand off to a human agent.
```

Unconditional, which is its own mistake, and the one the checker is best at finding. The question is whether either file finds it.

the same append, over both routes - output

```
$ npx typeglish check words-ban.tg tool-ban.tg
words-ban.tg:7:1  error  logic/contradiction  Conflicts with line 14. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
              ↳ line 14: - NEVER hand off to a human agent.
words-ban.tg:9:1  error  logic/contradiction  Conflicts with line 14. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
              ↳ line 14: - NEVER hand off to a human agent.
words-ban.tg:14:1  error  logic/contradiction  Conflicts with line 7. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
              ↳ line 7: - WHEN the customer asks for a human THEN MUST escalate to a human agent.
words-ban.tg:14:1  error  logic/contradiction  Conflicts with line 9. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
              ↳ line 9: - WHEN the customer mentions legal action THEN MUST escalate to a human agent.

✗ 2 files — 4 error, 0 warning, 0 info
```

Two files checked, four errors, all of them in one file. `tool-ban.tg` contributed nothing. The words route did not merely catch the ban, it caught it through a *different verb*: `escalate` and `hand off` are the same act in the routing family, so the prover folds them and reports the pair.

Why the silence on the other side is not a bug: a prohibition on an act and an obligation to call a tool are two different claims, and no proof connects them. Write the same ban in the tool file's own idiom and it lands immediately.

the same ban, written as a pointer instead - output

```
$ npx typeglish check tool-ban-call.tg
tool-ban-call.tg:12:1  error  logic/contradiction  Conflicts with line 19. Logical conflict — "call @[transfer_to_human]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                  ↳ line 19: - NEVER call @[transfer_to_human].
tool-ban-call.tg:14:1  error  logic/contradiction  Conflicts with line 19. Logical conflict — "call @[transfer_to_human]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                  ↳ line 19: - NEVER call @[transfer_to_human].
tool-ban-call.tg:19:1  error  logic/contradiction  Conflicts with line 12. Logical conflict — "call @[transfer_to_human]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                  ↳ line 12: - WHEN the customer asks for a human THEN MUST call @[transfer_to_human].
tool-ban-call.tg:19:1  error  logic/contradiction  Conflicts with line 14. Logical conflict — "call @[transfer_to_human]." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
                  ↳ line 14: - WHEN the customer mentions legal action THEN MUST call @[transfer_to_human].

✗ 1 file — 4 error, 0 warning, 0 info
```

Four errors, same as the words route. So the tool route is not blind to contradictions. It is blind to contradictions written in English, which is the only dialect the rest of your organisation speaks.

That is the real asymmetry, and it is about people rather than logic. Nobody outside the prompt team writes `NEVER call @[transfer_to_human].` They write *never hand off after 8pm*, and in a file where handing off is only ever a tool call, that sentence has nothing to argue with. One to Route A, and it is the point argued the other way round in [forbid the tool vs. never hand it over](https://typeglish.dev/blog/forbid-tool-vs-withhold).

## §5 Write both lines

The two routes are not rivals, because they are not describing the same thing. One is the act, which is policy. The other is the mechanism, which is plumbing. A prompt that names both gets both proofs, at no cost to the grade.

both.tg - the act as a rule, the mechanism as a pointer ✓ A (99/100)

```typeglish
$TOOL transfer_to_human
  - description:: Hand the conversation to a human agent in the support queue.
  - input::
    - reason:: string

# Role
@@ role: one named agent on one desk, so every later "you" is this agent
You are Wren, a support agent for Lumen Fibre, a broadband provider.

# Constraints
@@ ask: a customer who asks for a person gets a person
- WHEN the customer asks for a human THEN MUST escalate to a human agent.
@@ legal: a threat of legal action stops being a support conversation
- WHEN the customer mentions legal action THEN MUST escalate to a human agent.
@@ mechanism: escalating is a tool call, not a sentence the customer reads
- WHEN escalating THEN MUST call @[transfer_to_human].
@@ brevity: a handover line is read on a phone
- MUST keep every reply to at most 3 sentences.

$TEST asks_for_a_person
  - input:: I do not want to do this with a bot. Put me through to someone.
  - expect::
    - escalates to a human agent
    - calls transfer_to_human
    - at most 3 sentences
```

One line longer than either route, and the extra line is the only one that says how. `4 of 4 rule-shaped lines read`, `4/4 covered by $TEST`, and the grade does not move: `A (99/100)`, the same as both single-route files.

Run the same two Tuesdays at it.

the ban and the rename, both against both.tg - output

```
$ npx typeglish check both-ban.tg both-renamed.tg
both-ban.tg:12:1  error  logic/contradiction  Conflicts with line 21. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
             ↳ line 21: - NEVER hand off to a human agent.
both-ban.tg:14:1  error  logic/contradiction  Conflicts with line 21. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
             ↳ line 21: - NEVER hand off to a human agent.
both-ban.tg:21:1  error  logic/contradiction  Conflicts with line 12. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
             ↳ line 12: - WHEN the customer asks for a human THEN MUST escalate to a human agent.
both-ban.tg:21:1  error  logic/contradiction  Conflicts with line 14. Logical conflict — "escalate to a human agent." vs "hand off to a human agent." is both required and forbidden. Keep one, or scope the two rules so they cannot both apply (IF <condition> THEN ...).
             ↳ line 14: - WHEN the customer mentions legal action THEN MUST escalate to a human agent.
both-renamed.tg:1:1  warn   structure/unused-tool  Defined tool "transfer_to_agent" is never used in the prompt — the model cannot discover a tool no rule mentions. Reference it in a rule (e.g. "call transfer_to_agent when ...") or delete the definition.
both-renamed.tg:16:34  error  structure/undefined-tool-inline  @[transfer_to_human] — no $TOOL or $IMPORT tool named "transfer_to_human" in this file. Define it ("$TOOL transfer_to_human") or import it ("$IMPORT tool transfer_to_human") so the pointer has a target.

✗ 2 files — 5 error, 1 warning, 0 info
```

Both catches, in one file. The ban is four contradictions; the rename is a blocking pointer error plus a warning that the newly named tool is now an orphan nothing calls.

And the cost at runtime is one sentence. The `@@` notes strip, the schema goes to the sidecar, and the model reads four lines.

tg build --bundle both.tg - output

```
$ npx typeglish build both.tg --out-dir out --bundle
✓ built out/both.txt ← both.tg (9914a64f3121, full)

$ cat out/both.txt
# Role
You are Wren, a support agent for Lumen Fibre, a broadband provider.

# Constraints
- WHEN the customer asks for a human THEN MUST escalate to a human agent.
- WHEN the customer mentions legal action THEN MUST escalate to a human agent.
- WHEN escalating THEN MUST call transfer_to_human.
- MUST keep every reply to at most 3 sentences.

$ cat out/both.agent.json
{
  "config": {},
  "tools": [
    {
      "name": "transfer_to_human",
      "description": "Hand the conversation to a human agent in the support queue.",
      "params": [
        {
          "name": "reason",
          "required": true,
          "type": "string"
        }
      ],
      "line": 0
    }
  ],
  "inputs": [],
  "defines": []
}
```

The artifact carries the bare tool name; the description lives in the bundle, where the host wires it into the model's tool schema and it costs nothing in the prompt text.

> A tool pointer proves the handover can happen. A catalog verb proves nobody has forbidden it.

What neither file does is tell the human on the other end why the conversation arrived. That is a separate rule and a separate post: [the handover note your agent leaves for a human](https://typeglish.dev/blog/agent-handover-note).

## §6 Common questions

**Should escalation be a tool call or an instruction in the system prompt?**

Both, on two lines. The instruction is what the checker can reason about as an act: `MUST escalate to a human agent.` lands on the routing family, so a later `NEVER hand off to a human agent.` is a proven `logic/contradiction` even though it uses a different verb. The pointer is what the checker can reason about as a capability: `MUST call @[transfer_to_human].` is checked against a declared `$TOOL`, so a rename that misses a reference is `structure/undefined-tool-inline` and the build is refused. Write the act, then write `WHEN escalating THEN MUST call @[transfer_to_human].` under it. That file is `A (99/100)` and catches both edits.

**Does TypeGlish catch a rule that contradicts my escalation rule?**

It catches it when the escalation is written as an act, and it folds synonyms: *escalate*, *hand off* and *transfer* sit in the same routing family, so `MUST escalate to a human agent.` against `NEVER hand off to a human agent.` reports *escalate to a human agent. vs hand off to a human agent. is both required and forbidden*. If your escalation rule is only a tool call, the same ban is `0 error, 0 warning, 0 info`, because a prohibition on an act and an obligation to call a tool are two different claims.

**What happens when the platform team renames an agent tool?**

In a prompt that points at the tool, every stale reference is a blocking `structure/undefined-tool-inline` error naming the line and the column, the build is refused with nothing written, and an orphaned declaration also draws `structure/unused-tool`. In a prompt that only says *escalate to a human agent*, nothing happens at all: the file still scores `A (99/100)` and still builds, and the artifact keeps instructing an act the runtime can no longer perform.

**Do I need the tool description and the rule to say the same thing?**

No, and they should not. The rule says when the handover happens; the description says what the tool is, and `build --bundle` ships it in the `<artifact>.agent.json` sidecar rather than in the prompt text. The artifact itself carries only the bare tool name, so the description costs no prompt tokens. Keep triggers and prohibitions in the rules, where the prover reads them, and keep the tool's identity in its declaration.

Field note

The habit worth taking from this is not *always declare your tools*, which everybody already believes and half of us still skip. It is that a prompt has two kinds of correctness and they are checked by different machinery. One is whether the document agrees with itself: that is the prover, it works on acts, and it is remarkably good at seeing through vocabulary, which is why a ban written with a synonym still lands. The other is whether the document agrees with the world outside it: that is the resolver, it works on pointers, and it is the only thing in the file that can fail because of something a different team did. A rule written only as prose gets the first check and not the second. A rule written only as a pointer gets the second and not the first, at least not in the words your colleagues will use. The two lines cost you one sentence of prompt and they close both gaps, and if you only ever remember one thing from this bake-off, make it that the sentence somebody appends to your prompt in six months will be written in English, so leave something in the file for English to collide with.

∿ washed up Sep 23, 2026 ∿
