← All Notes

Related: It Isn't the Model. It's the Scaffolding. makes this same case about a coding agent staying inside your architecture. This is the version for everything else you ask a model to produce.

Here's a design principle worth having up front: building with AI is fractal. You start with a pattern and repeat it. Then you have groups of those patterns, and you repeat the groups. Then groups of groups. At every scale, you occasionally step back through the whole thing, check the focus, and look for the holes. That's the abstract version. Everything below is that same principle in concrete, practical form — the version you can start using today.

Give a chatbot a good brief and it writes a strong ad — or a status report, or a support macro, take your pick. Give it the same brief again for a different product, same session, and ask for the same format — a headline, three bullets, a closer — and the odds it actually matches are barely better than even. Start a new session and the odds get worse. The model isn't getting dumber. It's improvising the format from scratch every time, because nothing told it not to.

That's not a prompting failure. It's a structure failure, and prompting can't fix it.

The Fix Isn't a Better Prompt

Give the model a template and a brain dump, not a request and a hope. Hand it the exact shape you want — these fields, this order, this tone, this length — plus the raw specifics of the thing it's writing about, and it excels at extraction. It's not inventing a format under uncertainty anymore. It's filling in a form it already understands.

You can bake more into the template than structure. Design notes, usage instructions, the reasoning behind a field — all of it can live inside the template itself, so the model isn't just matching a shape, it's working from the same context a human filling out that form would have.

You could re-paste the template into a fresh chat every time. It works. It's also the same problem you're trying to solve, just moved one layer up — tedious, and easy to get slightly wrong under a deadline.

The better version of the same idea is a template a piece of software hands the model automatically, every time, through an API call built for exactly that job. A few keystrokes go in. A finished artifact comes out — copy, layout, images, all of it — because the structure was never left to chance in the first place.

It Isn't a Chatbot Trick

Say "prompt template" and most people picture a note somewhere with a good prompt in it, pasted into ChatGPT every time they need it. That's not what actually moves the needle, and it's worth being blunt about the gap: a saved prompt still needs a human to open a tab, paste it, wait, and copy the result back out. Every one of those steps is a place for the format to drift, and none of them can run while you're asleep.

The version that actually compounds the leverage skips the chat window entirely. A program calls the model directly — an API request, not a conversation — and the template lives in code instead of a note. Nobody is looking at a chat transcript. Something else triggers the call — a form submission, a cron job, a new row in a spreadsheet — and a finished result comes out the other end.

That's also why chatbots have free tiers and APIs mostly don't — the chat window is the demo. The API is the product.

This is where the fun begins. You do not need to be a software engineer to get there — a Python script, an API key, and maybe an afternoon spent understanding how the two talk to each other is the whole toll. People without a CS background pick this up in a handful of evenings, not a semester. The rest of this piece shows exactly what that code looks like — plainly, so you can see there's no trick being withheld.

The Fuller System

One template solves one output. The system that actually holds together is layered several levels deep, not just one — and it's the same fractal shape as the principle from the top of this piece, just drilled down instead of stated in the abstract.

At the top: a master voice guide and a master image guide, shared across everything. Neither one pins down a single fixed voice or a single fixed look — each one defines a range. Several legitimate registers. Several legitimate rendering modes. Not one style enforced everywhere.

A project picks a specific point inside that range and locks it down: this voice, this register, this palette, plus whatever templates that project's own outputs need. Two projects can share the exact same master guides and still read nothing alike, because each one pinned down a different point in the same range — and building that project-level guide was its own task that needed a real seed, a real decision about where in the range this project lives, not an autogenerated default. Same snake, eating its tail one level up: the seed a person injects into a single sentence is the same kind of seed a person injects into an entire project's style guide.

Below the project sits the task — an ad, an article, a status report, whatever the specific output actually is. A task carries its own human-generated idea, pulls in whichever template fits its shape, and inherits the voice and palette the project already locked down. This is where the seed from the next section actually gets spent: the project's guides constrain the shape and the register, but the task still needs a person to give it a reason to exist.

One layer further down, and it stops being about voice at all: rules for a specific output type, independent of any project. Every PDF this kind of system produces gets named with the title, spaces stripped, and the date in a fixed format — a convention with nothing to do with sounding like anyone, just a rule for a kind of file.

Four levels, one shape repeating: a range at the top, a pinned-down point below it, a seeded instance below that, and a mechanical convention underneath all of it. Recursive scaffolding, all the way down.

Stack the layers above a task and the tooling turns a few keystrokes into a finished piece. For something the system hasn't seen before — a genuinely new idea, not a variation on a known shape — you skip the template and do a longer content injection instead. You're teaching it something, not asking it to fill in a form.

The Formula

Templates plus style guides plus research keep output on-brand and factually sound. None of that, by itself, produces anything worth reading.

The missing ingredient is an opinion. A seed of actual disagreement, insider knowledge, or a take nobody else would have written. Without it, even a perfectly-formatted, perfectly-cited piece averages toward generic — correct, on-brand, and nobody's voice. Templates constrain the shape. They don't manufacture a point of view. That part still has to come from a person who has one.

A hand placing a single glowing red-orange seed into an otherwise cool blue mechanical template assembly, the seed the only warm color source in the frame

Free Text vs. a Schema

Once you're calling the API instead of pasting into a chat, the template stops being a paragraph you re-type and becomes a parameter you pass. Here's the same request, two ways — same model, same product, wildly different reliability.

Free text first. This is the version most people write without thinking about it:

free_text.py — unreliable shape
import openai

client = openai.OpenAI(api_key="sk-...")

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "user", "content": (
            "Write ad copy for a cordless drill. 20V, two batteries, "
            "LED work light. Give me a headline, three bullet points, "
            "and a closing line."
        )},
    ],
)

print(response.choices[0].message.content)

This works — the first time. Run it again for a different product and the shape shifts: bullets become a paragraph, the closer disappears, the headline runs long. Nothing broke. Nothing ever told the model to hold a shape in the first place.

Now the same request with an actual schema attached — the template made executable instead of implied:

structured.py — the template as a contract
import json
import openai

client = openai.OpenAI(api_key="sk-...")

AD_TEMPLATE = {
    "name": "product_ad",
    "strict": True,
    "schema": {
        "type": "object",
        "properties": {
            "headline": {"type": "string", "maxLength": 60},
            "bullets": {
                "type": "array",
                "items": {"type": "string"},
                "minItems": 3,
                "maxItems": 3,
            },
            "closer": {"type": "string", "maxLength": 100},
        },
        "required": ["headline", "bullets", "closer"],
        "additionalProperties": False,
    },
}

response = client.chat.completions.create(
    model="gpt-4o",
    response_format={"type": "json_schema", "json_schema": AD_TEMPLATE},
    messages=[
        {"role": "user", "content": "Product: cordless drill. 20V, two batteries, LED work light."},
    ],
)

ad = json.loads(response.choices[0].message.content)
print(ad["headline"])
print(ad["bullets"])
print(ad["closer"])

Same brief, different contract. Three bullets, every time, because the schema doesn't ask for three bullets — it rejects anything that isn't. JSON isn't a formatting preference here. It's how you hand the model a form it's graded against instead of a suggestion it's free to reinterpret.

You Don't Have to Write the Templates Yourself

You don't have to hand-build any of this from scratch. A model can build its own style guide — extract it from a pile of your own disjointed, unedited writing, the stuff you never intended anyone to read as a finished piece.

I did exactly that. The voice guide every article under my byline gets checked against wasn't written by hand — it was pulled from primary sources: a legal argument, a comedy routine, raw notes, voice memos, personal letters. And the material that turned out most useful wasn't the polished stuff. It was the gritty stuff — the unhinged 3 a.m. messages most people are smart enough to sleep on and never send in the morning, the off-the-cuff fragments I never edited for an audience. Unfiltered text carries more real voice than anything composed to be read.

A cluttered desk of scattered handwritten notes under warm lamp light, glowing threads of light lifting from the pages and weaving into a single organized structured document

That's the snake eating its tail, and it's a genuinely useful trick: use the model to extract the template that then constrains the model. You're not authoring structure from nothing. You're pointing the system at the rawest version of yourself you have and asking it to find the pattern that was already there.

One Call Isn't Enough

There's a second habit worth breaking once the output is code instead of a chat transcript: stop asking one call to do the whole job. "Write a good article, get every fact right, and sound like me" is three different skills stacked into one request, and a model that's good at one of them under that pressure is often worse at the other two. Split it up instead. A separate, narrower call for each concern — draft, voice, research, citations — each one small enough to actually grade.

This only works cleanly as separate API calls, not as separate messages typed into one chat thread. A chat carries its own history into every new turn by design — paste the draft, ask for a voice check, then ask for a citation check in the same conversation, and the second answer is already leaning on the first one instead of looking at the text fresh. A stateless API call doesn't have that problem unless you deliberately hand it history. Four narrow, independent jobs stay independent only if nothing leaks between them.

This is the other half of the fractal principle from the top of this piece: the periodic pass back through your own creation, checking focus and hunting for holes. Voice check and citation check are that pass, done in code instead of by memory, running every time instead of only when you remember to.

A wordless glowing workflow diagram: an open book, a color-prism, a gear-stamped scroll, a magnifying glass over papers, and a warm ember all feed converging light-conduits into a central stamping press, which ejects one finished glowing document card

Read left to right, that's the pipeline: the book is the voice guide, the prism is the image guide, the gear-stamped scroll is the project-specific rules, the magnifying glass is the research pass, and the ember — the one warm thing in the diagram — is the human opinion seed from two sections up. Everything feeds the press. Only one of those five inputs has to come from you every time.

Stay with me here — this is what that looks like as code, one small helper reused with a different job and a different schema each time it's called:

build_article.py — separate calls, separate jobs
def call(system: str, user: str, schema: dict) -> dict:
    """One shared helper. Every pass below is this function with a
    different system prompt and a different schema -- nothing more."""
    response = client.chat.completions.create(
        model="gpt-4o",
        response_format={"type": "json_schema", "json_schema": schema},
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
    )
    return json.loads(response.choices[0].message.content)

def research(topic: str) -> dict:
    """Pass one: pull real, checkable facts -- kept separate from writing."""
    return call(
        system="Return only verifiable facts with sources. No opinion, no phrasing.",
        user=topic,
        schema=RESEARCH_SCHEMA,
    )

def draft(topic: str, seed: str, template: dict) -> dict:
    """Pass two: template plus your seed, nothing else."""
    return call(
        system=f"Follow this shape exactly: {template}",
        user=f"Topic: {topic}\nAngle: {seed}",
        schema=template,
    )

def check_voice(text: str, style_guide: str) -> dict:
    """Pass three: does this sound like the guide, or like the topic?"""
    return call(
        system=f"Compare the text to this voice guide. Flag anything generic.\n{style_guide}",
        user=text,
        schema=VOICE_CHECK_SCHEMA,
    )

def check_citations(text: str, sources: list) -> dict:
    """Pass four: does every claim in the text trace back to a real source?"""
    return call(
        system=f"Sources available: {sources}. Flag any claim in the text a source doesn't back.",
        user=text,
        schema=CITATION_CHECK_SCHEMA,
    )

def build_article(topic: str, seed: str, template: dict, style_guide: str) -> dict:
    facts = research(topic)
    piece = draft(topic, seed, template)
    return {
        "article": piece,
        "voice_flags": check_voice(piece["body"], style_guide)["flags"],
        "citation_flags": check_citations(piece["body"], facts["sources"])["flags"],
    }

Four boring, narrow, separately-gradable jobs. None of them "write a great article." Together, they do — and when something's wrong, the flag tells you which pass to fix instead of leaving you to reread the whole thing guessing.

The Same Idea, One Level Down

This isn't just a content trick. It's the same principle a framework already applies to code. A Laravel blade is a template. Conventions are templates. Code comments and instruction files are the same idea aimed at a future reader instead of a future output — structure that survives the person who wrote it moving on to something else.

Claude Code is genuinely capable agency on its own. It's also unusually good at taking structure — give it a standing brief, a scoped set of rules, a template for how a project wants its output shaped, and it holds to it far more reliably than it improvises correctness from a single good prompt. Same lesson, aimed at a different kind of factory floor.

The prompt gets you one good result. The template, the guides, and the seed together get you the same good result on demand — and only one of those four ingredients has to come from you every time.

Zoom in or zoom out, it's the same shape: build the pattern, repeat it, group the patterns, repeat the groups, and circle back through the whole thing to check what's missing. Recursive scaffolding, the snake eating its own tail, all the way down.

— J.P. Howlett

This was written using Claude Sonnet and the Fire API.


Related:


Sources