In the first version of almost every AI feature, the output format is a line in the prompt: "respond in JSON with fields category and priority". It works most of the time. Then one day the model wraps the JSON in a friendly sentence, or invents a sixth category called "network", or returns a priority of "urgent" when the queue system only knows P1, P2 and P3. The downstream code either crashes or does something nobody intended.
The fix is not a firmer prompt. It is to stop treating the output format as a preference and start treating it as a contract: a written definition of what the model's answer must look like, enforced where it can be, checked in code where it cannot, and versioned like any other interface.
That shift sounds small. It is the single change that most clearly turns a model call into a software component.
Why "reply in JSON" is not enough
Umgeni Fibre is fictional, and its systems and figures are illustrative. In Part 2, Umgeni's classifier sends tickets to the right queue about 88% of the time on its labelled set. Its output feeds three things: the queue system, a dashboard the support lead watches and the weekly report to the operations manager. All three were built by people who never saw the prompt.
That is the real reason for a contract. The model's output is an interface between the model and other people's code. If the category list changes, the dashboard breaks. If priority arrives as a word instead of a code, the queue system misfiles it. A line in a prompt cannot protect those consumers. A schema, a validator and a version number can.
The contract has four layers
The prompt asks. It explains the job and describes each category in plain words, including the awkward boundaries: a router that will not connect after a power cut is a router fault, not an outage, unless the whole street is down.
The schema constrains the shape. Major model providers now let you pass a JSON Schema with the request and have the model's output constrained to it. OpenAI's documentation describes its Structured Outputs feature as guaranteeing adherence to the supplied schema, as opposed to older "JSON mode", which only guaranteed valid JSON. Anthropic's documentation lists its equivalent as generally available. Both were checked on 14 October 2026; read your own provider's current terms before relying on any of this.
Validation in code checks what the schema cannot express. Schema support is not uniform. Anthropic's documentation, for example, lists numeric range constraints such as minimum and maximum among the JSON Schema features it does not support. So "confidence must be between 0 and 1" is a rule your own code checks, along with anything that depends on your data: does this account exist, is this suburb one Umgeni serves, is the priority consistent with the category.
Business rules decide. Only this layer can cause something to happen. It decides to route, to send to a person or to reject, and it records why.
Umgeni's contract, version 2
Here is the illustrative schema Umgeni uses for triage. Its first version had four fields; the second added a flag for tickets that contain two problems, because the Part 2 results showed those were the commonest mistakes.
{
"type": "object",
"properties": {
"contract_version": { "const": "triage.v2" },
"category": { "enum": ["outage", "billing", "installation", "router_fault", "other"] },
"priority": { "enum": ["P1", "P2", "P3"] },
"sentiment": { "enum": ["calm", "frustrated", "angry"] },
"mixed_problem": { "type": "boolean" },
"confidence": { "type": "number" },
"reason": { "type": "string" }
},
"required": ["contract_version", "category", "priority", "sentiment", "mixed_problem", "confidence", "reason"],
"additionalProperties": false
}
Three design choices are worth copying.
Closed lists, not free text. Every field the downstream code acts on is an enumeration. The model cannot invent a sixth category because the schema has no room for one. If the business needs a new category, it is added to the contract, which means the queue system, dashboard and tests are updated at the same time.
A version inside the payload. contract_version lets the consumer reject output it does not understand and lets logs show which contract produced which decision. When Umgeni moved from v1 to v2, the old dashboard kept working because it ignored the new field and checked the version.
A short reason, used for review, not for routing. The reason field helps a person understand a decision quickly. No code branches on it. Treating free text as an input to logic is how a contract quietly stops being one.
What can still go wrong
A schema removes most parsing failures. It does not remove failure.
Providers document cases where output may not match the schema even with constraints on. OpenAI and Anthropic both describe refusals, where the model declines for safety reasons and the response may not follow your schema, and responses cut off at the token limit, which can be incomplete. Each needs a named path: log and send to a person for a refusal; retry once with a higher limit, then fall back, for truncation.
Then there are answers that are well-formed and wrong. A confidence of 1.7 is valid JSON and valid against a schema with no range constraint. A category of "billing" for a ticket that is plainly about a cut cable is valid and wrong. The first is caught by validation, the second by measurement against the labelled set over time. And finally, an answer can be valid, correct and still not allowed: Umgeni's business rule sends every billing ticket to a person regardless of the model's confidence.
The validator is small, and it is tested like any other code:
validate(result):
if result.contract_version != "triage.v2": fail("unknown contract")
if not (0 <= result.confidence <= 1): fail("confidence out of range")
if result.mixed_problem: fail("needs a person: two problems")
if result.category == "billing": fail("policy: billing reviewed by a person")
ok()
Notice that the last two are not really validation failures. They are business rules that happen to share the "send to a person" path. Keeping them in one small, readable function makes the rules auditable: anyone can see exactly when the model is overruled.
Test the contract, not just the model
Once the output is a contract, it can be tested without calling a model at all. Umgeni keeps a folder of recorded outputs, good and bad: a correct outage ticket, a refusal, a truncated response, a confidence of 1.7, an unknown contract version, a mixed-problem ticket. The validator and routing rules run against all of them on every code change, in under a second, for free.
The model itself is tested separately against the labelled set, as in Part 2, whenever the prompt, model or contract changes. Separating the two matters. When accuracy drops, you want to know immediately whether the model got worse or the code changed underneath it.
This is the same discipline spec-driven teams apply to the rest of their software. What a useful specification contains covers the wider practice; for an AI feature, the output contract is the most important part of the spec, because it is where the probabilistic part of the system meets the deterministic part.
What the stage makes worse
The contract adds work. Every category change now touches the schema, validator, tests and consumers together. That is friction, and it is the right kind: it makes changes deliberate. The team also notices that they now have two test suites to keep current, recorded outputs and the labelled set, and that nobody owns the labelled set formally. That gap goes on the list for Part 8.
Key points
Output format is an interface. Other people's code depends on it, so it deserves a contract, not a prompt line.
Four layers, one with authority. Prompt asks, schema shapes, code validates, business rules decide.
Schema support is real and uneven. Check what your provider enforces; range and data checks stay in your code.
Plan for refusal and truncation. Even constrained output has documented ways to miss the schema.
Test the contract without the model. Recorded good and bad outputs run on every change, in seconds.
Next: Part 4, Add memory only when the stateless design fails. All parts are on the series page.
How CloudNala can help
We help teams define the contract between their AI components and the rest of their systems: closed output schemas, validation for what the schema cannot express, business rules that are readable and auditable, and tests that run on every change. It is unglamorous work, and it is usually what decides whether an AI feature can be trusted by the systems around it.
Work with CloudNala
CloudNala helps organisations move from technology ambition to practical execution across cloud, AI, data, platform engineering and digital services.
Whether you are exploring AI, modernising your cloud environment, building a public-sector digital service, or turning an idea into a working MVP, we can help you shape the roadmap and deliver the next step.
Plan an AI MVP Build Sprint or write to us at consult@cloudnala.co.za