Writing spintax templates with AI

You pay a model once to write the template. After that the engine renders as many variants as you want, offline and for free. This page is about the part between those two facts: what to give the model, what to ask for, how to check what comes back, and the mistakes every model makes in spintax.

The short version

Give the model the documentation, give it your finished article, ask for one template. In that order. Everything below is detail on those three steps.

Note what the model is doing here: it is not writing, it is marking up text you already approved. Ask for both at once and you get a template assembled out of sentences nobody ever read as sentences. Write the article first, in one clean version you would publish as is. Then hand it over.

What to give the model

The site publishes its documentation in machine-readable form, so you do not have to explain spintax to anything. Pick the surface that matches your tooling.

SurfaceWhat it isUse it when
/llms-full.txt The documentation core in one file: the syntax reference, the nesting guide and all seven authoring guides. Just over 100 KB. Default. One fetch, or one paste into a chat.
spintax-authoring The same method condensed to two pages, written for a model rather than a reader. Your agent supports skills, or the context budget is tight.
/docs/variables.md and 19 others Any documentation page as clean Markdown. Add .md to the URL, or send Accept: text/markdown. One narrow question, mid-task.
https://spintax.net/mcp The MCP server: three tools that validate, render and analyze a template. You want the model to check its own work before you see it.

There are two other skills next to the authoring one. spintax-syntax covers the constructs and the trap in each. spintax-engines covers installing and calling an engine from JavaScript, PHP or Python. For authoring work you want the first one.

The prompt

This is the prompt we use. It assumes the model can fetch a URL; if yours cannot, paste llms-full.txt above it and delete the two lines that mention fetching.

You are converting a finished article into ONE spintax template.

Read the syntax and authoring rules first:
https://spintax.net/llms-full.txt
If you cannot fetch URLs, say so and I will paste the file instead.

Priorities, in this order:
1. Every rendered variant must be grammatical.
2. Every rendered variant must read like something a person wrote.
3. Variety comes last. A difference the reader would not notice
   is not worth a branch.

Hard rules:
- Never put a branch boundary inside a phrase whose grammar binds:
  subject and verb, preposition and object, article and noun.
- Do not repeat fixed text in both branches. Fork the smallest span
  that differs.
- Use %VAR% for anything that changes per site or per product.
- Use #def, not #set, when two references to the same value must agree.
- Use only the syntax in the rules above. Do not borrow from ICU,
  Handlebars or Jinja.
- Do not invent facts. If a slot needs a fact I did not give you,
  make it a variable.

Give me back:
1. The template, in one code block.
2. The variables you introduced, with an example value for each.
3. Anything in the article you could not express in spintax, and why.

Here is the article:
[paste your article here]

Two things about it are deliberate.

It points at the rules instead of restating them. The syntax is not in the prompt, and should not be. The documentation is one fetch away and it stays current; a copy pasted into a prompt six months ago does not.

It states priorities before rules. Left alone, a model optimizes for visible variety, because that is what a template looks like it is for. Grammar and readability have to be ranked above it explicitly, or you get four options in every slot and mush in the output.

Three ways to run it

A chat window

Works with any model that has a large context window. Paste llms-full.txt, paste the prompt, paste your article. The whole documentation core is around 100 KB, which is comfortable for current models but not free: if you are working through a long session, the authoring skill costs a fraction of that and covers the same method.

You will not get validation here. The model has no way to run the template, so treat its output as a draft and take it to the playground next.

An agent that can read the docs

In Claude Code, Cursor or any agent that browses, point it at the skill and let it pull individual pages as needed:

Load https://spintax.net/.well-known/agent-skills/spintax-authoring/SKILL.md
and follow it. Fetch the guides it links when you need the detail.

Every link inside the skill already points at a .md mirror, so the agent never has to parse a web page to read our documentation.

The MCP server

This is the setup worth the extra five minutes. https://spintax.net/mcp is a live MCP server, no authentication, three tools:

  • validate_spintax — diagnostics with severity, a stable code, and 1-based line and column. No errors means the template is safe to render.
  • render_spintax — up to 20 variants, with a seed for reproducible output and a variable map for the context.
  • analyze_spintax — what the template needs and contains: referenced variables, #set and #def definitions, includes, construct counts.

In Claude Code, one line:

claude mcp add --transport http spintax https://spintax.net/mcp

In a client that takes a config file:

{
  "mcpServers": {
    "spintax": { "type": "http", "url": "https://spintax.net/mcp" }
  }
}

Then add this to the end of the prompt:

You have the spintax.net MCP server. Before you show me anything:
call validate_spintax and fix every diagnostic with severity "error",
then call render_spintax with count 5 and read the variants yourself.
If a variant reads wrong, fix the template and repeat.

The difference is not cosmetic. Without the tools, the first template you see is the model's first guess, and you spend your review finding unclosed braces. With them, the model has already found the unclosed braces and you spend your review on whether the copy is any good.

Limits, so you can plan around them: 8 KB of template per call, 20 variants per render, and #include is disabled on the server. Compose big documents from sections and validate them one at a time. The full description of the server lives in its server card.

The loop that catches problems

Whatever tooling you use, the review is the same three steps, and the third one is the one people skip.

  1. Validate. Structural mistakes are cheap to find and the engine finds them for you: the playground underlines them as you type, validate_spintax returns them as data.
  2. Render twenty, not one. A template with six branches has hundreds of outcomes. One render tells you almost nothing, and it is usually the render that made you approve a broken template.
  3. Read them. Actually read the twenty. Check every conditional branch in both states, every plural form including the counts that select the rare one (in Russian: 1, 2, 5, 11, 21), and the shortest and longest permutation outcomes, where separator and spacing artefacts show up.

When something reads wrong, fix the template rather than the variant, then render twenty again. The model can drive this loop itself if it has the MCP tools, which is the whole reason to connect them.

What models get wrong

These are not random errors. They repeat across models, because they come from what the model learned elsewhere. Knowing the list turns a vague "this template feels off" into a two-minute check.

Note that almost none of them throw. The engine is lenient by design: a construct it does not recognize loses its braces and lands in the text as ordinary prose. The template validates clean and renders wrong, which is exactly why the loop ends with reading variants and not with a green check.

1. It cuts the branch through bound grammar

The most common failure, and the one that produces text that is wrong rather than merely dull. A subject and its verb, a preposition and its object, an article and its noun cannot be varied independently.

{Our platform|Our tools} {is|are} built for small teams.
→ four combinations, two of them ungrammatical

{Our platform is|Our tools are} built for small teams.
→ two combinations, both correct

Detail: grammar-safe synonymization.

2. It reaches for #set when the references must agree

A #set value is a macro: it re-rolls at every reference. Models pick it by default because it reads like an assignment in a programming language.

#set %tool% = {Slack|Jira|Linear}
Connect %tool% in one click. %tool% syncs both ways.
→ "Connect Slack in one click. Linear syncs both ways."

#def %tool% = {Slack|Jira|Linear}
Connect %tool% in one click. %tool% syncs both ways.
→ "Connect Jira in one click. Jira syncs both ways."

Detail: variables and multi-site reuse.

3. It writes plurals in the syntax of some other library

Models have seen far more ICU MessageFormat than spintax, and it shows. They also default to two forms, which is wrong for Russian, Ukrainian, Belarusian and Serbian.

We support {count, plural, one {# language} other {# languages}}.
→ "We support count, plural, one # language other # languages."

We support %n% {plural %n%: language|languages}.
→ "We support 3 languages." / "We support 1 language."

The two halves of this mistake behave very differently, which is worth knowing. Borrowed ICU is the quiet kind of wrong: no diagnostics at all, because a brace group without a pipe is legal, so the engine drops the braces and prints what was inside. Wrong arity is the loud kind: Russian takes three forms, {plural %n%: язык|языка|языков}, and supplying two under a Russian locale returns a plural.arity error and leaves the construct unrendered in the output. Same habit, and only one half of it is caught for you.

Detail: plural agreement.

4. It invents a conditional

Handlebars and Jinja habits produce {if …} blocks that the engine renders as literal text.

We ship worldwide. {if %HasCrypto%}We also accept crypto.{/if}
→ "We ship worldwide. If 1We also accept crypto. /if"

We ship worldwide. {?HasCrypto?We also accept crypto.}
→ "We ship worldwide. We also accept crypto."

The first render is what a leaked construct looks like: the braces are gone, the variable was substituted where nobody wanted it, and the closing tag became a word.

A conditional is driven by a value. When the choice is genuinely arbitrary, {a|b} is correct and a conditional is not.

Detail: conditional spintax.

5. It confuses square brackets with a choice

[a|b|c] shuffles and joins all three items, separated by a space unless you configure otherwise. It does not pick one. A model that means "pick one" and writes brackets produces a sentence with three adjectives where you wanted one.

Try our [fast|simple|affordable] tool.
→ "Try our fast simple affordable tool."

Try our {fast|simple|affordable} tool.
→ "Try our simple tool."

Try our [<minsize=2;maxsize=3;sep=", ";lastsep=" and ">fast|simple|affordable] tool.
→ "Try our simple, affordable and fast tool."
→ "Try our simple and fast tool."

Detail: permutations in practice.

6. It spins every clause

Given no priority, a model treats density of variation as the goal and puts a branch in every phrase. The output stops sounding like anything.

{Our|The} {platform|system} {helps|lets} {teams|groups} {ship|release}
{faster|quicker|sooner}.

The variants that matter are the ones a reader would notice. Two well-placed branches in a paragraph beat twelve inside one sentence. This is what the priority block in the prompt is for.

7. It repeats the fixed part in both branches

Duplicated text is an invitation to drift: the next edit changes one branch and forgets the other.

{Spintax (spin syntax) is|Spintax — "spin syntax" — is} a template language.
→ "Spintax" and "is" duplicated across both branches

Spintax {(spin syntax)|— "spin syntax" —} is a template language.
→ one copy of the fixed words, only the difference forked

Detail: the reverse authoring mindset.

8. It drops external values straight into the context

Variable values are markup-capable by default: the engine re-parses a value that contains {, [ or %. A product name from a database, a user-supplied string, a title with a stray brace, and your template renders something you never wrote. Pass untrusted values through neutralize() before they reach the context.

Detail: variables and multi-site reuse.

9. It writes one enormous template

Ask for a whole landing page and you get a single unreviewable block. Past a screenful, split it: an item template renders one unit, a section template assembles items, an orchestrator assembles sections. Each piece stays testable on its own, and the MCP server's 8 KB limit stops being a limit.

Detail: template composition.

Before you ship the template

  • Twenty variants rendered and actually read.
  • Every conditional seen in both states.
  • Every plural form seen, including the rare bucket.
  • No #set where two references must agree.
  • Every variable the template references has a value at render time.
  • Untrusted values neutralized.
  • The shortest and the longest permutation outcome both punctuate correctly.

What this costs

One template is one conversation with a model, and the rendering that follows is free: the engine runs locally, deterministically, with no API call per variant. That is the entire economic argument for this workflow, and it is worked through model by model, with current prices, in what AI content actually costs.

Then render it

The template you just made runs unchanged on every engine in the family: @spintax/core in JavaScript, spintax/core in PHP, spintax-core in Python, and the WordPress plugin. Same syntax, same output, one shared test corpus. See the four engines to pick one.