← Tidelines/Guides

How to write a severity scale your agent can apply

Every contact centre runs on a ladder: severity one to four, tier one to three, stage one to three of a dunning flow. Prompts inherit the ladder from a runbook, and runbooks have a habit of describing the top rung and the bottom rung and trusting a person to fill in the rest.

by TypeGlish team8 min read#guides
Define the middle.

TL;DR Define every level of the scale, not just the ends: prompt/scale-gap proves the hole (defined at one and three but never two), and filling it moves a hosting-support prompt from C (76/100) to B (80/100). Then stop numbering the levels: a $REQUIRE variable severity: one of critical, degraded, question with a $SWITCH ON arm per member turns a missing level from something nobody notices into a blocking compile error.

The ladder is the most reused object in contact-centre work and the least written down. It lives in a runbook, in a routing rule, in a QA rubric, and in the head of whoever has been there longest, and it reaches the agent prompt as two or three sentences that sound complete. This guide is about the level in the middle: how to find it, how to write it so it checks, and how to make the next level somebody adds impossible to ignore.

§1The scale with a hole in it

Here is a first-line technical support prompt for a hosting provider. It has a role, a tool, two routing rules and a length bound, and it reads like somebody who knew the job wrote it.

sev1.tg - two rungs of a three-rung ladder✓ compiles
<$CONFIG>
  $IMPORT tool page_oncall
</$CONFIG>

# Role
You are a technical support agent for Meridian Cloud, a hosting provider.

# Constraints
- Severity 1 is a full service outage.
- Severity 3 is a question about a feature.
- WHEN a ticket is severity 1 THEN you MUST page an on-call engineer with @[page_oncall].
- WHEN a ticket is severity 3 THEN you MUST answer from a help article.
- MUST keep every reply to at most 4 sentences.
Nothing in this file is wrong. It is also the shape a ticket at severity two hits: a scale with a defined top and a defined bottom, a hard rule at each end, and a middle the model has to interpolate. It will interpolate, and it will do it per conversation.
tg check and score sev1.tg - output
$ typeglish check sev1.tg
sev1.tg:9:1  warn   prompt/scale-gap  Scale gap - "severity" is defined at one and three
  but never two (with line 8). A model must guess the middle: define severity two, or
  state the whole scale.

 1 file - 0 error, 1 warning, 0 info

$ typeglish score sev1.tg
sev1.tg - C (76/100)  proven errors: none  tiers: base+z3
  planes  runtime 85 (what the model reads) · hygiene 50 (source only)
  facets  enforceability 70 x.21 · hardness 100 x.12 · directness 93 x.08
          consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene)
          style 50 x.08 · security 100 x.08
The finding is reported at file line 9, the first definition, and the message points at line 8 for the second one. Both are right in their own scheme: range.line counts the real file, and the cross-reference inside the message counts the file with blank lines removed. Your prompt is a graph, not a list documents the same two schemes in one score report, and it is worth knowing before you spend ten minutes looking at the wrong line.
A scale you define at the ends is a scale you have asked the model to interpolate.

§2What the checker reads as a scale

Before you go grepping, know what fires, because this reader is deliberately narrow and knowing its edges is the difference between trusting it and being surprised by it. It looks for a definition, not a mention: one of seven nouns, a number, and a copula.

  • Seven nouns, and only seven. severity, level, tier, priority, stage, phase, grade. Each one fires on the same shape. Urgency 1 is a full service outage. beside Urgency 3 is a routine question. is 0 error, 0 warning, 0 info, because urgency is not on the list.
  • The number has to be a number. Severity 1 and Severity one both fire. P1 is a full service outage. beside P3 is a question about a feature. draws nothing, which matters because P1 is how most people actually write it.
  • Three copulas. Severity 1 is a full service outage., Priority 1 means a full service outage., and MUST treat stage 1 as a first reminder. all fire. The definition can sit inside a rule; it does not need a line of its own.
  • The gap can be anywhere inside. Define one, two and four and the message reads defined at one and two and four but never three. Define one and four and it reads never two or three. Only the interior is checked, so a scale that starts at two is not a gap.

Read that list as the honest limit rather than as a bug list. The check is a floor on the ladders it knows, in the spelling a runbook uses when it is being formal. If your house style is P1, the mechanical way to buy the check back is to write the definitions out once in the prompt in the long form, which you probably want anyway: P1 means nothing to a model that has not been told, a point Your agent has never heard of a wallboard makes about every other piece of internal shorthand in the file.

§3Defining the middle is not the same as routing it

The cheap fix is one line, and you should make it first, because a level with no definition is a level nobody has decided.

sev1b.tg - the middle defined✓ 0 error, 0 warning, 0 info
<$CONFIG>
  $IMPORT tool page_oncall
</$CONFIG>

# Role
You are a technical support agent for Meridian Cloud, a hosting provider.

# Constraints
- Severity 1 is a full service outage.
- Severity 2 is a partial loss of function.
- Severity 3 is a question about a feature.
- WHEN a ticket is severity 1 THEN you MUST page an on-call engineer with @[page_oncall].
- WHEN a ticket is severity 3 THEN you MUST answer from a help article.
- MUST keep every reply to at most 4 sentences.
0 error, 0 warning, 0 info at B (80/100), from C (76/100), with style 50 to 100. One detail is worth not skipping: enforceability goes down, 70 to 65, because the file now carries three statements that describe the world and two that oblige anybody, and enforceability is measured over statements. A definition is not a rule, and the score is honest about that.

Now look at what the clean run does not say. Severity two is defined and has no routing rule at all. A ticket in the middle of the ladder has a meaning and no destination, and the file is 0 error, 0 warning, 0 info. That is not a hole in the checker so much as a hole in prose: nothing in English obliges a level to have a rule, so nothing can prove one is missing. The definition gap is catchable because a numbered scale has an arithmetic shape. The routing gap only becomes catchable when the levels stop being prose and become a type.

§4Make the levels a type

Declare the scale as a closed domain on a required input, and put one arm per member in a $SWITCH ON block. Two things change. The levels get names instead of numbers, which removes the interpolation problem at the source, and coverage stops being something you check and becomes something the compiler proves.

sev-v2.tg - one arm per level✓ A (93/100)
<$CONFIG>
  $IMPORT tool page_oncall
  $REQUIRE variable severity: one of critical, degraded, question
</$CONFIG>

# Role
@@ role: first-line technical support for a hosting provider
You are a technical support agent for Meridian Cloud, a hosting provider.

# Constraints
@@ routing: the host classifies the ticket, so the compiler picks the arm and ships one
$SWITCH ON @{severity}
  - critical:: You MUST page an on-call engineer with @[page_oncall] within 5 minutes.
  - degraded:: You MUST post an update on the status page within 30 minutes.
  - question:: You MUST answer from a help article and cite a link to it.
@@ brevity: four sentences keeps a support reply scannable
You MUST keep every reply to at most 4 sentences.
@@ no_estimates: a restoration time in a reply is heard as a promise
You MUST NOT state a restoration time.

$TEST critical_pages
  - input:: Every request to our API is timing out.
  - expect::
    - contains "engineer"

$TEST question_cites
  - input:: Where do I rotate an API key?
  - expect::
    - contains "article"
The numbers are gone and nothing was lost with them: critical, degraded and question are the same three rungs with the interpolation removed, because there is no arithmetic between two names. Each arm carries a real bound (5 minutes, 30 minutes, a cited link) rather than a level number, which is the part a reply can be graded against.
tg score and test --dry sev-v2.tg
$ typeglish score sev-v2.tg
sev-v2.tg - A (93/100)  proven errors: none  tiers: base+z3
  planes  runtime 91 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 70 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

$ typeglish test sev-v2.tg --dry
 sev-v2.tg  coverage: 0/2 rules exercised
  · critical_pages - "Every request to our API is timing out." (not run)
       contains "engineer"
  · question_cites - "Where do I rotate an API key?" (not run)
       contains "article"

$ typeglish build sev-v2.tg --vars '{"severity":"critical"}'
 built .typeglish/dist/sev-v2.txt ← sev-v2.tg (82ca1de09566, full)

$ typeglish build sev-v2.tg --vars '{"severity":"question"}'
 built .typeglish/dist/sev-v2.txt ← sev-v2.tg (bbe9a07512f5, full)

# the "question" artifact, in full:
# Role
You are a technical support agent for Meridian Cloud, a hosting provider.

# Constraints
You MUST answer from a help article and cite a link to it. You MUST keep every reply to at most 4 sentences. You MUST NOT state a restoration time.
Two levels, two hashes, one arm in each artifact: the model is never handed a ladder to place a ticket on, only the rung it is standing on. The coverage: 0/2 is a wrinkle rather than a failure, and it is the right number: the arms are compile-time, so the only statements counted as rules across every build are the length bound and the estimate prohibition, and neither test exercises those. If you want coverage to move here, write the case against the rule that survives every arm.

The payoff is not the grade. It is the four edits this shape refuses, and each one is an edit somebody makes to a routing prompt within a year of shipping it.

tg check - four one-line edits to sev-v2.tg
# 1. delete the "degraded" arm
sv-drop.tg:12:1  error  structure/non-exhaustive-switch  @{severity} can be degraded,
  but no arm handles it - add a "- <member>::" row for each (or a deliberate
  "- otherwise::" fallback).

# 2. add "planned" to the domain and nothing else
sv-add.tg:12:1  error  structure/non-exhaustive-switch  @{severity} can be planned,
  but no arm handles it - add a "- <member>::" row for each (or a deliberate
  "- otherwise::" fallback).

# 3. drop the ": one of ..." and leave the input untyped
sv-open.tg:12:1  error  structure/opaque-switch  @{severity} has an open domain
  (untyped) - a $SWITCH on it needs a "- otherwise::" row (members can't cover an
  open domain).

# 4. add a "- otherwise::" arm for the levels we have not written yet
sv-otherwise.tg:16:3  error  structure/unreachable-otherwise  Every member of
  @{severity} has its own arm - "- otherwise::" can never run, and it would silently
  absorb members added to the domain later. Remove the row.
Edit 2 is the one to keep in mind, because it is the future version of the defect this post opened with. Somebody adds a rung to the ladder in the routing config, and every switch that reads the domain refuses to build until a human decides what the new rung does. Edit 4 is the same argument from the other side: an otherwise arm is a rung you have agreed not to think about, and the compiler names exactly that consequence, it would silently absorb members added to the domain later.

Four steps, in the order that costs least. Grep your prompts for severity, priority, tier, stage, phase, level and grade, and run check to see whether any of them are defined at the ends only. Write the missing definitions in long form, in the runbook’s words. Then decide who owns the classification: if the host does, promote the scale to a $REQUIRE variable with a closed domain and switch on it; if the conversation does, keep the prose definitions and give every level a rule with a bound in it, since a level with a definition and no destination is the gap wearing a disguise. And when the ladder grows, let it fail loudly, which it will as long as no otherwise arm is sitting there to catch it.

§5Common questions

How do I write severity levels in an AI agent system prompt?
Define every level, not just the ends, and then stop numbering them. Severity 1 is a full service outage beside Severity 3 is a question about a feature is a prompt/scale-gap warning reading defined at one and three but never two, and filling the middle clears it and lifts the file from C (76/100) to B (80/100). The stronger form drops the numbers entirely: declare $REQUIRE variable severity: one of critical, degraded, question and give each member an arm in a $SWITCH ON block, because a named member cannot be skipped without a compile error while a number in prose can.
What counts as a scale in a TypeGlish prompt?
A definition, not a mention. The reader looks for one of seven nouns followed by a number and a copula: severity, level, tier, priority, stage, phase and grade, each firing on shapes like Severity 1 is a full service outage, Priority 1 means a full service outage, or MUST treat stage 1 as a first reminder. Digits and number words both count. Outside that list nothing fires, so P1 is a full service outage draws nothing and neither does Urgency 1, which makes the check a floor on the scales it knows rather than an audit of every ladder in your file.
Why does my agent handle a mid-priority ticket inconsistently?
Usually because the prompt defines the ends of the scale and leaves the middle to inference. A model reading severity one is a full outage and severity three is a question has to interpolate severity two, and it interpolates per conversation, which is why the same ticket lands in different places on different days. Defining the middle is only half of it: a file that defines all three levels but writes routing rules for only two checks at 0 error, 0 warning, 0 info, because nothing in prose obliges a level to have a rule. Making the levels a typed domain and switching on them is what turns the missing route into a blocking error.
Should I use an otherwise arm for the severity levels I have not written yet?
No, and TypeGlish refuses to compile one. Put - otherwise:: on a $SWITCH whose domain is fully covered and it is a blocking structure/unreachable-otherwise reading it can never run, and it would silently absorb members added to the domain later. That is the whole argument for a closed domain: with no fallback, adding planned to the domain six months from now is a blocking structure/non-exhaustive-switch at every switch that reads it, which is the loud version of the failure. An otherwise arm is the quiet version, and quiet is what put the gap in the scale in the first place.
Field note

prompt/scale-gap and the tier overlap in a refund authority table are the two ways a ladder copied out of a spreadsheet goes wrong, and they are opposites. A gap is a rung with no owner; an overlap is a rung with two. Both are advisory rather than blocking, both leave consistency at 100 because neither is a contradiction, and both are priced on style, which means the CI line that catches either one is score --min B rather than a strict flag.

∿ washed up Aug 29, 2026 ∿