Teardown: Hi {first_name}, thanks for contacting us
Nine lines, lifted out of a flow builder and pasted into a repo. Five of them carry a placeholder. The checker has two things to say, and neither of them is about a placeholder.
TL;DR A {placeholder} in a prompt file is prose, not a reference, so a prompt with five unfilled holes checks at 0 error, 0 warning and the braces reach the model verbatim. Add one character each and the same file is 5 blocking structure/undefined-ref errors; declare them with $REQUIRE variable and the built artifact comes out byte-identical, so the coverage costs nothing at runtime.
This one arrives by a route almost every midmarket CX team takes. The first version of the agent gets built in a no-code flow builder, because that is the fastest way to prove the thing works. Then it works, traffic grows, and somebody wants the prompt in git next to the rest of the stack. So the system prompt gets copied out of a text area and pasted into a file. What comes across is the words. What does not come across is the half of the platform that was quietly filling in the blanks.
§1The export, and the two findings it gets
Here is the file, unedited apart from the company name. Ava is a first-line assistant for a mobile network: greet, answer, try one fix, hand off to a human on request.
# Role You are Ava, the virtual assistant for {company_name}. # Constraints - ALWAYS open with "Hi {first_name}, thanks for contacting us". - MUST answer in {language}. - You should try to resolve the issue before you offer a handoff. - NEVER approve a goodwill credit above {refund_cap}. - WHEN the customer asks for a human THEN transfer to {queue_name}.
ava.tg:7:14 info prompt/hedging Hedging — "try to" turns this instruction into a suggestion the model may skip. Delete the hedge, or commit to a modal (MUST / NEVER / SHOULD). ava.tg:7:1 info prompt/unintroduced-definite "the issue" retrieves something this document never introduces - a model must guess which issue is meant. Introduce it on another line ("You manage an issue.") or name it outright. ✓ 1 file — 0 error, 0 warning, 2 info
Read that again, because it is the whole teardown in one observation. The compiler found the two defects in the one rule that was written by hand, and said nothing whatsoever about the five rules whose meaning depends on a value that is not in the file. The hedge is real and worth fixing. It is also the least interesting thing wrong with this file.
ava.tg — C (75/100) proven errors: none tiers: base+z3
planes runtime 84 (what the model reads) · hygiene 50 (source only)
facets enforceability 70 x.21 · hardness 78 x.12 · directness 88 x.08 · consistency 100 x.17
structure 100 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 75 x.08 · security 100 x.08
lever annotation 0/100 (up to +12 overall) — Put a "@@ why" note directly above each statement
("@@ name: why" also names it).
structure 100, and the biggest lever the scorer can see is missing documentation. Nothing in the scorecard has a way to represent a file that is five values short of a meaning.§2The sigil is the declaration
So why is the checker quiet? Because {first_name} is not a placeholder. It is the word first_name with a brace either side of it, and a brace is a character like any other. The convention that braces mark a substitution point is an agreement between you and your templating layer, and nothing else in the pipeline was in the room when you made it.
TypeGlish has a spelling for a reference, and it is not braces. It is @{name}: an at sign plus a typed bracket. Take the same nine lines and add that one character to each hole, changing nothing else.
ava.tg:2:40 error structure/undefined-ref Dangling pointer — @{company_name} names nothing. Declare it with "$REQUIRE variable company_name" (a runtime variable), or "company_name IS …" (a fixed value). ava.tg:5:24 error structure/undefined-ref Dangling pointer — @{first_name} names nothing. ava.tg:6:18 error structure/undefined-ref Dangling pointer — @{language} names nothing. ava.tg:8:41 error structure/undefined-ref Dangling pointer — @{refund_cap} names nothing. ava.tg:9:55 error structure/undefined-ref Dangling pointer — @{queue_name} names nothing. ✗ 1 file — 5 error, 0 warning, 2 info
A hole nobody declared is not a hole. It is a string that happens to have braces in it.
The diagnostic is doing something better than complaining. It is asking the question the export never answered: which kind of hole is this? It names both answers, and they are genuinely different things. $REQUIRE variable company_name says the value arrives at runtime and the host must supply it. company_name IS Cobalt Mobile says the value is a fact about this deployment and belongs in the file. A flow builder never made you choose, because it filled both kinds from the same variables panel. This is the same argument as your system prompt is an API contract, arriving from the other direction: the contract exists whether or not you wrote it down, and the only version anything can enforce is the one you wrote down.
§3What the model actually receives
Warnings and info do not block, so the export builds. Build it, because the artifact settles the argument about whether any of this matters.
✓ built .typeglish/dist/ava.txt ← ava.tg (27b73a9ddba9, full) # Role You are Ava, the virtual assistant for {company_name}. # Constraints - ALWAYS open with Hi {first_name}, thanks for contacting us. - MUST answer in {language}. - You should try to resolve the issue before you offer a handoff. - NEVER approve a goodwill credit above {refund_cap}. - WHEN the customer asks for a human THEN transfer to {queue_name}.
Hi {first_name}.That is the line customers see when the substitution step misses: Hi {first_name}, thanks for contacting us. It happens for boring reasons. The host reads firstName and the prompt says first_name. The contact came in on a channel that carries no name. Somebody added a rule with a sixth placeholder and did not add it to the panel. None of those is a prompt bug, and all of them are invisible in the prompt, which is the point: the file gives you no way to tell a filled hole from an unfilled one, so there is nothing to review.
Worth being precise about what a declared variable does to the artifact, because it is less than people expect. A $REQUIRE variable is a runtime hole: the compiler tracks it, checks it, and then lowers it back to {name} in the built prompt for the host to fill. A declaration with a value (company_name IS Cobalt Mobile) is a compile-time hole and expands to the value. Same syntax at the reference site, two completely different lifetimes.
§4Either side of a hole
Two failures live on either side of a declared variable, and the export has both shapes available to it.
The first is the mirror image of what we just saw: a variable the platform declares that no rule reads. Flow builders accumulate these, because a variables panel is cheap and deleting a variable feels risky. Unlike the undeclared hole, this one the compiler catches.
orphan.tg:3:3 warn structure/unused-import Required variable "account_tier" is never used. orphan.tg:3:21 warn clarity/unused-variable $REQUIRE variable "account_tier" is never used — no @{account_tier} reference fills it. Remove it, or reference it in the prompt. orphan.tg:4:3 warn structure/unused-import Required variable "last_order_id" is never used. orphan.tg:4:21 warn clarity/unused-variable $REQUIRE variable "last_order_id" is never used — no @{last_order_id} reference fills it. Remove it, or reference it in the prompt. ✓ 1 file — 0 error, 4 warning, 0 info $ typeglish check orphan.tg --strict ✗ 1 file — 4 error, 0 warning, 0 info
--strict. This is the direction where a variables panel gets audited for free.The second failure is the one that costs money, and it is in line 8 of the export. NEVER approve a goodwill credit above {refund_cap} reads like a limit. It is not a limit, because the limit is not in the file. Here is a controlled pair: two files identical except for what stands where the cap goes.
# Role You are a billing support agent for Cobalt Mobile. # Constraints - MUST approve a goodwill credit of at most {refund_cap} on your own authority. - MUST approve a goodwill credit of at least 100 dollars for a repeat outage.
0 error, 0 warning, 0 info.# Role You are a billing support agent for Cobalt Mobile. # Constraints - MUST approve a goodwill credit of at most 50 dollars on your own authority. - MUST approve a goodwill credit of at least 100 dollars for a repeat outage.
logic/numeric twice, blocking, and Z3 names both bounds. The conflict was always there. Only one of the two files puts it where a prover can reach it.This is not a gap in the checker, it is arithmetic. A solver compares values, and a placeholder is not a value. Which yields the rule worth taking away from the whole file: per-conversation data belongs in a hole, and policy belongs in the prompt. A first name, a locale, a queue name and a company name genuinely vary per contact. A goodwill cap does not vary per contact; it varies per quarterly review, which is a git commit, not a runtime input. Parameterising it bought flexibility nobody asked for and gave away every proof about it.
§5The rebuild, and the hole import will not touch
The mechanical part of the fix is a command. typeglish import lifts bare holes to declarations deterministically, which is what it shipped in 0.6.0 to do.
$ typeglish import ava-export.md --out ava.tg
imported -> ava.tg
lifted: company_name, language, refund_cap, queue_name
0 errors · 0 warns · 2 info (was 0 · 0 · 2)
the worklist:
optimization
L12 info prompt/hedging: Hedging — "try to" turns this instruction into a suggestion the
model may skip. Delete the hedge, or commit to a modal (MUST / NEVER /…
L12 info prompt/unintroduced-definite: "the issue" retrieves something this document never
introduces - a model must guess which issue is meant. Introduce it on another line…
✓ builds clean — next: typeglish build ava.tg
first_name is missing, and the reason is exact rather than accidental. It is the one hole sitting inside a quoted literal: ALWAYS open with "Hi {first_name}, thanks for contacting us". A quoted literal is a verbatim zone, and the importer will not reach inside one to rewrite your text, which is the same promise that makes quotes safe for approved copy in the first place.
The same boundary shows up a second time, in the check. Declare all five by hand, leave the quotes, and the file reports a warning that looks wrong:
ava-declared.tg:3:3 warn structure/unused-import Required variable "first_name" is never used. ✓ 1 file — 0 error, 1 warning, 2 info
Both behaviours follow one rule, and once you know the rule the fix is obvious: quote only what has to be verbatim, and keep your holes outside the quotes. In this case the quotes were doing nothing anyway. The rule wanted the customer greeted by name; it did not need to dictate the sentence, and if the exact wording is brand copy it belongs in an $EXAMPLE where it can be demonstrated rather than mandated.
One more thing worth proving before the rewrite, because it is the whole reason to bother. Take the export and add nothing but the five declarations. Change no word of any rule. Then build both files.
$ typeglish build ava.tg ✓ built .typeglish/dist/ava.txt ← ava.tg (27b73a9ddba9, full) $ typeglish build ava-declared.tg ✓ built .typeglish/dist/ava-declared.txt ← ava-declared.tg (27b73a9ddba9, full) $ diff .typeglish/dist/ava.txt .typeglish/dist/ava-declared.txt $
diff has nothing to say. The declarations are hygiene-plane work: they buy the compiler five references to track and change not one byte the model reads. Compare your prompt has a second reader, where a whole grade of difference also compiled to identical artifacts.So the coverage is free, and there is no deployment risk to trade against it. Here is the file rebuilt: five holes declared, four of them still holes, the cap written out as policy, the hedge committed to a modal, the issue named, and a reason over every rule.
<$CONFIG> $REQUIRE variable company_name $REQUIRE variable first_name $REQUIRE variable language $REQUIRE variable queue_name </$CONFIG> # Role You are Ava, the virtual assistant for @{company_name}. # Constraints @@ greeting: naming the customer in the opener measurably lifts survey scores - ALWAYS greet @{first_name} by name in your first message. @@ language: the host resolves the locale from the channel, so the agent never guesses - MUST answer in @{language}. @@ self_serve: a handoff costs an agent seat, so spend one step trying not to need it - MUST offer one troubleshooting step before you offer a handoff. @@ goodwill_cap: 50 dollars is the limit a supervisor signed off, and it is policy, not per-conversation data - NEVER approve a goodwill credit above 50 dollars. @@ handoff: a request for a human is honoured, never negotiated - WHEN the customer asks for a human THEN transfer to @{queue_name}.
0 error, 0 warning, 0 info. Four variables, all referenced; one former variable now a literal the prover can use. refund_cap is gone from the panel entirely, which is the edit ops will query and the one worth defending.C (75/100) to A (91/100): runtime 84 to 91, hygiene 50 to 92. The remaining lever is enforceability 68, and it is honest, because two of these five rules are conditionals and a conditional is harder to enforce than a bound. What the grade does not show is the part that mattered. Before, a missing value was a customer-visible string. After, a missing value is a red build.
§6Common questions
- Why does my AI agent say Hi {first_name} to customers?
- Because nothing in the pipeline knew that was a hole. Curly braces are a convention your templating layer understands, and they mean nothing to anything else: to a prompt compiler they are ordinary prose, and to the model they are five characters of literal text. If the substitution step is skipped, misspells the key, or receives no value for it, the braces travel all the way to the model and the model reads them out. TypeGlish will not warn you about a bare
{first_name}either, for the same reason. Write it as@{first_name}with a$REQUIRE variabledeclaration and the hole becomes a reference the compiler tracks, so a name nothing declares is a blockingstructure/undefined-referror. - Do curly braces do anything in a system prompt?
- Only for whatever fills them. Braces are not a language feature of a prompt, they are a marker your host runtime agreed to look for, and the prompt file itself has no opinion about them. That is exactly the problem: a marker with no declaration cannot be checked, counted, or proven present. In TypeGlish the checkable spelling is
@{name}and the declaration is$REQUIRE variable name. Both single{name}and double{{name}}braces check at0 error, 0 warning, 0 info, which is worth knowing if you assumed one dialect was safer. - Should a policy number live in a placeholder or in the prompt?
- In the prompt, if it is policy. A number inside a hole is invisible to the prover:
MUST approve a goodwill credit of at most {refund_cap}besideMUST approve a goodwill credit of at least 100 dollarschecks at0 error, 0 warning, 0 info, and the identical file with50 dollarswritten in place of the placeholder is 2 blockinglogic/numericerrors. The compiler can only compare values it can see. Keep genuinely per-conversation data in a hole (a first name, a queue, a locale) and write policy limits as literals, where a conflicting rule added six months later fails the build. - How do I find unfilled placeholders in a prompt before it ships?
- Convert them into declared references, then let the compiler do it every build.
typeglish importlifts bare{holes}to$REQUIRE variabledeclarations with@{name}pointers deterministically, and from then on any hole nothing declares is a blocking error rather than a silent string. Runtypeglish check --strictas the gate, because the mirror case (a variable your platform declares that no rule reads) is a warning on a plain check and a blocking error under strict. The declaration is free at runtime: the artifact still carries{name}for the host to fill, byte for byte.
Every false green has the same shape, and this one belongs in the collection next to why your agent follows half a rule. There, a newline broke a statement in half so a bound never reached the prover. Here, five braces mean a value never reached the file. Different mechanism, identical symptom: 0 error, and a checker being scrupulously honest about a smaller file than the one you thought you handed it. The habit that catches both is to ask what the checker could actually see, and the cheapest way to answer that is typeglish build. The artifact is not a summary of your prompt. It is your prompt.