MuninMunin
Sign inStart free
Home/Journal/Your content model is a prompt.
Engineering · 8 min read

Your content model is a prompt.

Move an article body from one markdown field to an ordered array of typed blocks and the front end barely notices. The author does — and increasingly the author is an agent. What a schema can enforce, what it cannot, and what every block type costs you.

Angled down at a bright empty modern living room floor: a squared stack of identical pale oak panels, tidy groups of a few distinct cut parts, a half-built shelving frame made only from those shapes, someone kneeling beside it — the declared set bounds what can be built.
A fixed set of parts decides what can be built.

Declare fifteen block types for an article body and an author may legally use one of them fifteen times in a row. A wall of prose validates. The schema has nothing to say about it, because nothing is wrong — monotony is a legal document in a blocks-based CMS the same way a thousand-line function is legal C. That is the failure mode you inherit when the author is an agent, and it is the interesting half of this post. Start with the half that worked.

Why replace a markdown field with typed blocks?

Because the author changed. The default content model for an article is one field holding a markdown string. The alternative is an ordered array of typed blocks — a lead, a pull quote, a callout, a code block, a set of takeaways — each with declared fields and its own validation. Swap one for the other and the front end barely notices. The thing writing into it notices immediately.

The conventional case for structured content is about output: you want to render the same article to web, to email, to an app, to print, and a markdown string bakes in assumptions about HTML. That case is real and it is not the one I'm making. The case I care about is the input side. When the thing composing an article stops being a person with a text editor and becomes a process holding a tool schema, the content model stops being storage and starts being instructions. I've made that argument about interfaces before; this is the same shift arriving at the content layer, where it is a great deal less abstract.

What does a markdown string actually let an author do?

Anything. John Gruber released Markdown in 2004 as a way for a person to write something that would end up as HTML, and its defining property is permissiveness — unmatched syntax degrades into literal text rather than failing. A markdown column will accept an empty string, a novel, six raw <div>s, and a pull quote faked out of a blockquote. All of it is valid, because the column has no opinion.

That permissiveness is a feature when a human is typing and an editor is reading. It is a liability when the author is a process that will do exactly what the last thing it read implied, and no human sees the output before it lands in a database.

jsonthe shape of it
// one field, one string, anything goes
{ "body": "## Why we did it\n\nMost B2B platforms grow by acquisition…" }

// an ordered array of typed blocks
{ "body": [
    { "key": "lead",  "type": "lead",  "props": { "text": "…" } },
    { "key": "why",   "type": "prose", "props": { "markdown": "## Why…" } },
    { "key": "quote", "type": "pull_quote",
      "props": { "quote": "…", "source": "…" } },
    { "key": "costs", "type": "editorial_list",
      "props": { "items": ["01 | …", "02 | …"] } }
] }

Why does the shape of the body matter more when the author is an agent?

Because the schema is the only brief the author is guaranteed to read. A human writer picks up house style from a style guide, an editor, and three years of reading the publication. An agent gets the content model — field names, types, and descriptions — loaded into its context, and not much else. Whatever isn't in the schema isn't in the brief.

Once you accept that, the field description stops being documentation and becomes the highest-leverage text in the system. The useful ones are not descriptions of data. They are editorial rules, written where the author cannot fail to encounter them.

An example. Say you declare two similar block types: one for key/value rows where the key is a short token, another for rows where the key is a whole phrase. A description that says rows of key and value is a wasted field. A description that says the token version is right when the key is a two-letter tag against one or two lines, that the phrase version is right when the value runs to a short paragraph, and that if the values run past four lines you should use neither — prose with subheads, or a real table — is a paragraph of editorial judgment sitting inside a JSON schema. It is the difference between an author who knows when to reach for the block and an author who reaches for it because it was in the list.

There's a smaller case I like more, because the rule it encodes is written down nowhere else. Give the schema both a title and a display_title, and put the rule on the second one: set it when the title runs past about fifty characters. That field exists because a title now has two readers. The long form has to carry the query someone would type or ask an assistant; the short form has to sit under a masthead without wrapping to three lines. That isn't a style note in a document somebody maintains. It's a field.

A style guide is a document somebody might read. A schema is a document the author cannot avoid.

What can a typed body enforce that a style guide can't?

Four things, and they're worth separating because only one of them is about validation. A schema makes a class of error structurally impossible, gives every unit of content a stable address, moves rendering decisions out of the author's hands, and carries its own instructions. A style guide can only ask.

  • Structural impossibilityA pull_quote without a quote is not a badly formatted pull quote. It fails validation and never reaches the database. The error class disappears instead of being caught in review — which matters most for the errors nobody reviews, like a stats block with two values where the layout expects three.
  • Every block has an addressBlocks carry a stable keyintro, costs, faq. That turns editing into a patch rather than a rewrite: an agent correcting a price in one section sends that block alone, not the whole article, and cannot silently drop a paragraph it forgot to include in the resend.
  • Rendering is decided elsewhereThe author chooses callout with tone warn. It does not choose a colour, a border, an icon, or a width. Presentation lives in the renderer, where a design decision can be changed once instead of being relitigated in every article that happens to contain a warning.
  • The instruction travels with the fieldA description attached to a field arrives in the author's context every single time, in the same payload as the thing it describes. A wiki page about tone of voice arrives never.

What a schema cannot enforce

Taste, which is the part I opened with. A body made entirely of prose blocks is valid, and it is the model's default: given a list of block types and an argument to make, a language model will reach for the most general container available and keep reaching for it. Type systems have never been able to tell you that a correct program is a boring one.

The fix isn't a stricter schema, and I want to be clear about that because the instinct is to add a constraint. You cannot express use the pull quote when the sentence is doing more work than the paragraph around it in JSON Schema, and a rule like no more than three consecutive prose blocks produces an author that inserts a divider every third paragraph to satisfy it. Goodhart applies to content models. What actually works is writing the expressiveness requirement into the instructions the agent runs under, and then reading the output.

The second thing a schema cannot check is whether anything in it is true. A stats block declares three strings. It does not declare three facts. A structured body makes a fabricated number better formatted, more prominent, and more likely to be quoted back by something that reads it later. Structure raises the cost of a hallucination rather than lowering it.

Descriptions are code with no tests

The moment a field description stops matching the real editorial rule, you have built a confident liar that ships on every request. Say a description tells the author that the numbering on a list block is generated and must never be typed by hand. If a renderer change ever makes that false, nothing throws — every future article quietly hand-numbers its lists, and the first person to notice is a reader. Field descriptions are the part of the schema with no type checking, no test suite, and the widest blast radius. Review them like code, because they are.

How does the agent actually learn the shape?

By fetching it. The block types are defined on the collection, and the agent reads them the way it reads anything else — a call that returns the content model as data. The write tool itself takes a fairly opaque data object and the server validates what arrives against the collection. So the brief reaches the author as a payload it looks up at runtime, not as a type declared in the tool's signature.

There are only two places a schema can live, and the choice matters more than it looks.

Put it in the tool's inputSchema and it is declared once, in the catalogue, where every caller sees it before making a call. The model cannot write without having been shown the shape. Put it in data returned by a read tool and it can change without redeploying anything, it can be versioned alongside the content, and — the deciding factor — it can differ per tenant.

That last one settles it for a hosted platform. One endpoint serves many organisations, and their collections are not the same. You cannot bake one tenant's content model into a tool signature that every tenant reads. The cost is real and worth naming: nothing forces the model to fetch the collection before it writes. An author that skips the lookup is guessing, and the only thing standing between a guess and the database is server-side validation. Which is an argument for validating strictly, and against treating the tool description as though it were the contract.

jsona block union, in ordinary JSON Schema
{
  "type": "object",
  "properties": {
    "body": { "type": "array", "items": { "$ref": "#/$defs/block" } }
  },
  "$defs": {
    "block": {
      "oneOf": [
        { "$ref": "#/$defs/lead" },
        { "$ref": "#/$defs/prose" },
        { "$ref": "#/$defs/pull_quote" }
      ]
    },
    "pull_quote": {
      "type": "object",
      "required": ["type", "props"],
      "properties": {
        "type": { "const": "pull_quote" },
        "props": {
          "type": "object",
          "required": ["quote"],
          "properties": {
            "quote":  { "type": "string" },
            "source": { "type": "string" }
          }
        }
      }
    }
  }
}

If your content model is fixed at build time, the other option got better recently and it's worth knowing. SEP-2106, in the 2026-07-28 MCP specification, lifts tool inputSchema and outputSchema to full JSON Schema 2020-12: input schemas keep the type: "object" root constraint but now allow composition through oneOf, anyOf, and allOf, plus conditionals and $ref/$defs. A discriminated union like the one above is expressible in a tool signature, which means a single-tenant server can put the whole shape in the catalogue and let the protocol carry it. That isn't the trade we're in, but it's the better one when you can take it.

How does this compare to Portable Text, Contentful Rich Text, and Strapi dynamic zones?

Nobody here invented structured content. Blocks are old news in the headless CMS world; what's new is who benefits from them. Each of the mature implementations makes a different trade, and the trades read differently once your author is a machine.

Sanity's Portable Text got there first and has aged the best. It models rich text as an array of blocks with styles and mark definitions, explicitly designed to serialise into any human-readable format rather than assuming HTML, and it allows arbitrary custom objects in the root array. It is a genuinely well-made format and worth reading before you design your own — Sanity is a content platform, so the content model is the product there in a way it is not anywhere else on this list.

Contentful Rich Text stores a nested JSON document tree that you walk to render. It supports embedded entries and assets properly, which markdown cannot. The costs are a renderer library, a custom renderer for every embedded type, and payloads considerably larger than the equivalent markdown. For a generator, depth is the awkward part: a flat array of typed blocks is easier to produce correctly than a tree, because the author never has to hold a nesting position in its head.

Strapi dynamic zones are the closest match to the model described here — a declared set of components the author picks from, returned as structured JSON. The cost surfaces in retrieval rather than authoring: Strapi 5 removed the shared population strategy, so you declare population per component with on fragments instead of leaning on populate=*.

Payload stores Lexical editor state as JSON, with blocks living as nodes carrying a fields object, and gives you converters to get to HTML.

So why would you use Munin's CMS instead?

Not for the editing experience. Munin ships no visual page builder, no WYSIWYG rich-text editor and no plugin marketplace. Those four products are built around exactly that, and if a human editor sitting in a browser all day is the centre of your content operation, one of them is the right purchase and I would not argue you out of it.

Use Munin when the thing you actually want to know is which article closed the deal.

A headless CMS holds documents. It has no opinion about who read them, because the people are in a different system with a different idea of what a person is, and the join between them is an integration somebody maintains. Munin's CMS is one of six modules on one Postgres, so the article, the visitor who read it, the conversation they opened two days later, and the deal that came out of it are rows in the same database against one contact record, with row-level security carrying the tenancy — which is the whole architectural bet. The questions that are normally a data project are queries: what someone read before they got in touch, which piece support keeps sending, which topic produces conversations that convert.

The second half is that the same 204 MCP tools that wrote the article can do everything around it. One agent drafts the piece, generates the cover, checks what the last one earned, finds the contacts who read it, and leaves a follow-up in a review queue for a human to approve or kill. No orchestration between those steps, because there is nothing to orchestrate — it's one endpoint over one schema, and the tool catalogue is the surface. That isn't something you bolt onto a headless CMS with three integrations and a scheduler. It's what you get when the CMS was never a separate product.

And it's MIT throughout — no enterprise/ directory holding the features you'd actually need. The whole thing runs on your own hardware straight from the repository, or on Munin Cloud in the EU if you would rather not run it at all. The exit is a sequence of tool calls and every module has a matched export. We'd rather you could leave.

What it costs

  • 01Every block type is forever. Fifteen types is fifteen renderers, fifteen validation paths, and fifteen cases every future migration has to handle. Adding one is a five-minute schema change and a permanent liability, which is the same arithmetic as adding a public API method.
  • 02Diffs get worse. A markdown body diffs like prose in any tool written in the last forty years. A block array diffs like JSON — a one-word fix shows up as a changed line inside a quoted string, and a reordering shows up as a rewrite. Stable block keys and per-entry versioning make review survivable, and it is still worse than git diff on a text file.
  • 03Humans are slower in it. Nobody has ever written faster in a structured editor than in a plain text file. If most of your authors are people and most of your articles are ordinary essays, a markdown column with a good style guide is the correct engineering decision and you should not read the rest of this list as an argument against it.
  • 04The schema becomes the ceiling. When the right shape for a thought isn't in the list, the author reaches for prose and the thought comes out flatter than it should have. A markdown string has no ceiling — it will let a writer invent a form on the spot. That freedom is precisely what you give up, and it is the cost I notice most often.

Look at the shape of that bill, though. Every line on it is paid once, at the moment you declare a type or wire a renderer, and none of it recurs per article. The recurring cost sits on the other side of the trade.

A markdown column gives you exactly one editorial control that runs on every piece, and it is a person reading the piece. That works, and it is the correct answer for a team of human writers. It stops working the moment article production is automated, because "a good style guide" is then a document that nobody in the loop opens — the author is a process, and a process reads the payload it was handed. The schema is the only brief that arrives with the work, every single time, whether anyone remembered to send it or not.

So the four costs above are finite, enumerable, and mostly one-off. The alternative's cost is a review step you have to staff forever, on every article, at whatever volume you publish. That is the arithmetic that decided it here, and it is the arithmetic worth running before you decide it for yourself.

Frequently asked questions

Should I store markdown or structured blocks in my CMS? Store markdown if your authors are people and your output is one website. Store structured blocks if you render to more than one surface, or if something other than a person composes the body. The deciding question is not how the content is displayed — it's who or what is writing it, and whether anyone reads the result before it publishes.

What is Portable Text and how is it different from Markdown? Portable Text is Sanity's open format for rich text as an array of typed blocks, each with a style and a set of mark definitions, and it permits arbitrary custom objects in the array. Markdown is a plain-text syntax from 2004 that assumes an eventual conversion to HTML. One is data you query and transform; the other is a string you parse.

Can an AI agent write reliably into a structured content model? Yes for structure, no for judgment. Schema validation reliably prevents malformed blocks, missing required fields, and wrong types, so those errors stop reaching production. It does not prevent monotony, bad section ordering, or invented numbers, because none of those are type errors. Structure is a floor under quality, not a substitute for review.

How does an AI agent discover a CMS's content model? Either the shape is declared in the tool signature the model sees before it calls anything, or it's returned as data by a read tool the model has to remember to call. Multi-tenant platforms are pushed towards the second, because different customers have different collections and one signature can't describe them all. The trade is that nothing forces a lookup, so the server has to validate strictly.

What is an agentic CMS? One an AI agent can operate directly, rather than one with an AI writing assistant bolted into the editor. In practice that means every action is a tool the agent can call, the content model is discoverable, and writes land in a reviewable state instead of going straight live. Munin exposes 204 such tools at one endpoint and keeps content versioned so a human approves what publishes.

Can a CMS tell me which article generated a customer? Only if the content and the customer live in the same database. A headless CMS holds documents and knows nothing about readers, so answering that means an integration between the CMS, the analytics tool, and the CRM. Munin runs content, analytics, conversations, and CRM as six modules on one Postgres against one contact record, which makes it a query.

Is a blocks-based CMS worth it for a small team? Only if the agent is doing the writing. For a two-person team publishing a handful of posts by hand, a markdown field costs less to build, less to render, and less to edit. The economics flip when article production is automated, because then the schema is the only editorial control you have that runs on every single piece.

How do you stop an agent from using the same block type for everything? Not with the schema — a wall of prose is valid content. It has to be handled in the instructions the agent runs under, stated as a requirement to use the type range expressively, and then checked by reading the output. There is no way to encode this as a constraint that doesn't produce worse writing than it prevents.

If you're modelling content for an agent author

  • When something other than a person composes the body, the content model stops being storage and becomes the brief — treat the schema as a prompt, because that is how it is consumed.
  • Field descriptions are the highest-leverage text you will write. They arrive in the author's context every time; a style guide arrives never.
  • Typed blocks buy four things: impossible errors, a stable address per block so edits are patches, rendering decisions kept away from the author, and instructions that travel with the fields.
  • They do not buy taste or truth. A wall of one block type validates, and a stats block requires three strings, not three facts.
  • Decide where the schema lives. In the tool signature it is unavoidable but fixed; in fetched data it is per-tenant and versionable, but nothing forces the author to read it — so validate on the server either way.
  • Know the terms before you take the trade: every block type you declare is permanent, JSON diffs need stable keys and per-entry versioning to review well, and the declared set bounds the forms available. Those costs are paid once, at declaration, and they do not recur per article.
  • Pick Munin when you want the article, the reader, and the deal in one database and 204 MCP tools an agent can drive — MIT, self-hostable, free to start.

The block types this article is written in are declared on the collection itself, readable in the repository or by calling cms_list_collections against an org of your own.

Your style guide is a suggestion. Your content model is the prompt.

Notes

  1. Full JSON Schema 2020-12 for tool schemas landed in SEP-2106, announced in the 2026-07-28 specification release notes.
  2. Sanity's case for structured text over markdown: Why Portable Text is awesome and the introduction to Portable Text.
  3. Contentful's rich text document model is documented under What is rich text?; Strapi's component-based bodies under Dynamic Zones; Payload's Lexical JSON under Rich Text.