unit U1 — 1 of 4
Prompt contracts
system prompts, structured JSON output
A prompt contract is the written agreement between your flow and the model: the system prompt fixes the model’s role, its rules, and what it must never do, while a response schema fixes the exact shape of what comes back. In production this matters because your downstream steps don’t read prose — they read fields. The moment an Edit Fields step or a Branch references {{ step_2.category }}, that key has to exist, spelled the same way, every single time, or the branch silently takes the wrong path.
You assemble the contract inside an HTTP Request Piece pointed at the LLM endpoint, with the API key living in a Connection, never a step parameter. The messages array carries a system entry for the rules and a user entry for the payload you built upstream. To force structure, send a response_format with a JSON schema rather than politely asking for JSON in the prose — the API then constrains the tokens to valid fields. A following Code Piece parses the body and hands clean, typed keys to the rest of the flow.
Where it breaks: assuming the words “return JSON” are enough. Without an enforced schema, a helpful model wraps its answer in Markdown fences, prefixes it with a chatty “Sure, here you go”, or quietly renames a field — and JSON.parse throws on the first run that drifts. The flow’s run history shows a red step and a payload you never designed for. Pin the schema at the API layer, and validate the parsed object before anything downstream trusts it.
worked example
A support-desk automation classifying every inbound ticket into a fixed triage schema before routing.
{
"model": "{{ connections.llm.model }}",
"max_tokens": 512,
"system": "You are a support-triage classifier. Populate only fields defined by the schema, never invent categories, and when unsure choose needs_human.",
"messages": [
{ "role": "user", "content": "{{ trigger.body.ticketBody }}" }
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "triage",
"schema": {
"type": "object",
"properties": {
"category": { "type": "string", "enum": ["billing", "bug", "feature", "needs_human"] },
"priority": { "type": "string", "enum": ["low", "med", "high"] },
"summary": { "type": "string", "maxLength": 120 }
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
}
}
}field checklist
- Put the API key in a Connection, never a step parameter.
- Send a system message that fixes role, rules, and forbidden outputs.
- Enforce output with a JSON schema, not a prose request.
- Validate the parsed object before any downstream step reads it.
- Set additionalProperties false so extra fields fail loudly.
common failure — Model returned prose, not JSON
A classifier shipped with only “respond in JSON” in the prompt and no schema. For a week it worked; then a long ticket made the model prepend an explanation, JSON.parse threw, and the router dumped every reply into the fallback branch. The fix is structural: enforce a json_schema at the API, keep additionalProperties false, and validate the parsed object in a Code Piece before routing on it.
check your understanding
Asking for JSON in the prose works until the day the model wraps its answer in Markdown fences. Which schema keyword makes an unexpected extra field fail loudly instead of flowing downstream?
next unit opens once this is passed
sandbox validation
The check above confirms you followed the unit. Marking the module COMPLETED takes more: build the automation in your own engine and submit the exported flow and its run evidence, signed, to your unique validation URL. See the module page for that spec.