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.
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.
<$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.
$ 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
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.besideUrgency 3 is a routine question.is0 error, 0 warning, 0 info, because urgency is not on the list. - The number has to be a number.
Severity 1andSeverity oneboth fire.P1 is a full service outage.besideP3 is a question about a feature.draws nothing, which matters becauseP1is how most people actually write it. - Three copulas.
Severity 1 is a full service outage.,Priority 1 means a full service outage., andMUST 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.
<$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.
<$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"
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.$ 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.
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.
# 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.
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 outagebesideSeverity 3 is a question about a featureis aprompt/scale-gapwarning reading defined at one and three but never two, and filling the middle clears it and lifts the file fromC (76/100)toB (80/100). The stronger form drops the numbers entirely: declare$REQUIRE variable severity: one of critical, degraded, questionand give each member an arm in a$SWITCH ONblock, 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,phaseandgrade, each firing on shapes likeSeverity 1 is a full service outage,Priority 1 means a full service outage, orMUST treat stage 1 as a first reminder. Digits and number words both count. Outside that list nothing fires, soP1 is a full service outagedraws nothing and neither doesUrgency 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$SWITCHwhose domain is fully covered and it is a blockingstructure/unreachable-otherwisereading 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, addingplannedto the domain six months from now is a blockingstructure/non-exhaustive-switchat every switch that reads it, which is the loud version of the failure. Anotherwisearm is the quiet version, and quiet is what put the gap in the scale in the first place.
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.