← Tidelines/Deep dives

Your system prompt is an API contract

The prompt you hand a model is not a memo. It is an interface: it declares the inputs it needs, the tools it is promised, and the shape it returns. Read it that way and a whole class of runtime surprise becomes a compile error.

by TypeGlish team6 min read#deep-dives
Declare the surface. Sign the contract.

TL;DR Treat a system prompt like a function signature: every @{input} is a required argument, every @[tool] is a promised capability. Declare them with $REQUIRE variable and $IMPORT tool, and a reference with no declaration becomes a compile error (structure/undefined-ref or structure/undefined-tool-inline) instead of an empty placeholder in production.

Nobody ships a function that calls an argument it never declared. The compiler would stop you before you got out of the editor. Yet we ship prompts that do exactly this all the time: a rule says greet the customer by name and reaches for a name the host was never told to pass, or an instruction says look up the order and points at a tool that doesn’t exist in this deployment. The prose reads fine. It runs fine, right up until the one input that isn’t there. The fix isn’t more careful reading. It’s noticing that a prompt has a signature, and holding it to one.

§1The interface you’re already writing

A system prompt never runs alone. Something wraps it: an orchestrator that fills in the customer’s details, a tool layer that exposes functions, a caller that expects a certain kind of answer back. That surrounding code and the prompt have to agree, and the points where they touch are a contract whether you wrote it down or not.

TypeGlish gives you the vocabulary to write it down. Three constructs carry the whole interface:

  • Inputs - $REQUIRE variable names a slot the host must fill at call time, and @{name} reads it. This is the argument list.
  • Capabilities - $IMPORT tool declares a function the runtime provides, and @[name] calls it. This is the set of dependencies you’re allowed to reach for.
  • Output - an $EXAMPLE pins the shape of a correct answer, the way a return type pins what a function hands back.

None of this is new syntax you have to adopt wholesale. It’s a lens: the moment you see @{customer_name} as a parameter this prompt requires rather than a word with braces, the question “who supplies this, and did they agree to?” becomes impossible to skip.

A prompt with an undeclared input isn’t underspecified. It’s a broken call waiting for the wrong argument.

§2A contract nobody checks is a contract you break

Here is a perfectly ordinary billing-agent prompt. It reaches for the customer’s name and an order-lookup tool. Read it and nothing looks wrong, which is the whole problem.

billing.tg (the implicit contract)✗ blocked
# Role
You are a billing-support agent for Acme.

# Instructions
- ALWAYS greet the customer by name using @{customer_name}.
- WHEN a customer asks about an order, THEN look it up with @[get_order].
Two references, zero declarations. In prose this passes review, because prose has no notion of “declared.” The compiler does: @{customer_name} and @[get_order] are both pointers at targets that were never introduced.
tg check - output
billing.tg:5:43  error  structure/undefined-ref  Dangling pointer - @{customer_name} names nothing.
  Declare it with "$REQUIRE variable customer_name" (a runtime variable), or "customer_name IS ..." (a fixed value).
billing.tg:6:61  error  structure/undefined-tool-inline  @[get_order] - no $TOOL or $IMPORT tool
  named "get_order" in this file. Define it ("$TOOL get_order") or import it ("$IMPORT tool get_order").

 1 file - 2 error, 0 warning, 0 info
This is the type checker doing its job. structure/undefined-ref is “you used an argument that isn’t in the signature”; structure/undefined-tool-inline is “you called a function that isn’t in scope.” Both are the kind of mistake a compiled language never lets ship, and a prose prompt always does.

Notice what would have happened without the check. In production, @{customer_name} renders to whatever the orchestrator passes, and if the orchestrator was never told to pass it, that’s an empty string. The agent opens with “Hi , ” on the exact conversation you most wanted to feel personal. The tool pointer is worse: it names a capability this deployment doesn’t have, so the model either hallucinates the lookup or stalls. Neither failure throws. They just quietly happen to customers.

§3Declare the surface, and the pointers resolve

Honoring the contract is two lines at the top of the file. State the input the host must fill, and the tool the runtime provides. The references downstream don’t change at all; they simply have targets now.

billing.tg (the explicit contract)✓ compiles
$REQUIRE variable customer_name: string
$IMPORT tool get_order

# Role
You are a billing-support agent for Acme.

# Instructions
- ALWAYS greet the customer by name using @{customer_name}.
- WHEN a customer asks about an order, THEN look it up with @[get_order].
Compiles clean. The two declarations are the signature made visible: this prompt takes a customer_name string and depends on a get_order tool, and anyone wiring it up can read exactly that off the top of the file instead of grepping the prose for surprises.

The payoff is not tidiness. It’s that the agreement is now mechanical. If the orchestrator team renames the field to full_name and forgets to update the prompt, the build goes red on the pointer, not silent on the greeting. The contract stopped being a thing two teams remember and became a thing the compiler enforces, which is the entire reason function signatures exist in the first place. It is the same split the compile-time and runtime planes draw: the declarations are scaffolding the model never reads, and the values they promise are what actually reaches it.

§4Contracts are checked on every edit, not just the first

A signature earns its keep over a codebase’s life, not on the day it’s written, and so does a prompt’s. The value of declaring @{customer_name} and @[get_order] isn’t that today’s file is correct. It’s that the next fifty edits are held to the same interface: add a rule that reaches for @{account_tier} and the build reminds you the host has to supply it; delete a tool import that a rule still calls and the build catches the dangling call before a customer does.

That is what separates “an interface” from “a comment describing an interface.” A comment drifts; a declaration the compiler reads cannot. If a rule and its dependencies disagree, there is no build to deploy. For the capabilities half of the contract specifically, this is also how you stop the quieter bug of a tool that’s declared but never actually reached from a rule, which we pulled apart in why your agent won’t use the tool you gave it.

Write the prompt as a contract, and “the runtime changed” turns from an incident into a failed build.

You don’t have to declare everything to get this. Start with the two references that would hurt most if they were empty: the input your greeting depends on, the tool your core task calls. Give them a signature. Everything else you can add the day a rule reaches for it, because now the compiler will tell you the day a rule reaches for it.

§5Common questions

What does it mean that a system prompt is an API contract?
It means the prompt declares an interface with the runtime around it: the inputs it requires ($REQUIRE variable), the tools it is promised ($IMPORT tool), and the output it guarantees. Like a function signature, that interface can be checked, so a reference to an input or tool that was never declared is a dangling pointer and TypeGlish blocks the build with structure/undefined-ref or structure/undefined-tool-inline.
Why does my agent reference a variable or tool that isn't there?
Because prose lets you write @{customer_name} or @[get_order] without ever declaring them, and nothing at authoring time objects. The mismatch only surfaces at runtime, when the placeholder renders empty or the tool call has no target. Declaring the input with $REQUIRE variable and the tool with $IMPORT tool turns that runtime surprise into a compile error you see before you ship.
How do I declare the inputs and tools a system prompt depends on?
Put the contract at the top of the file: $REQUIRE variable customer_name: string for each input the host must fill, and $IMPORT tool get_order for each capability it provides. Then reference them with @{customer_name} and @[get_order]. Every pointer now has a declared target, so the compiler can prove the prompt and its runtime agree.
What is the difference between a runtime variable and a fixed value?
A $REQUIRE variable is a slot the host fills at call time (the customer's name, their tier, their time zone); a name IS value declaration is a constant baked in at compile. Use $REQUIRE for anything that changes per conversation and a fixed declaration for anything that is the same on every call. Both give @{...} a target; they differ in who supplies the value and when.
Field note

The dangling-pointer proofs behind structure/undefined-ref and structure/undefined-tool-inline ship in the core checker from TypeGlish 0.1.0: every @{...}, @<...>, and @[...] must resolve to a declared target, or the file will not build. For a worked example of writing that contract from a blank file, see building a refund agent, prompt-first.

∿ washed up Jul 23, 2026 ∿