Spintax Syntax Reference

Complete reference for the spintax format, also called spin syntax: every construct, the rules it follows and the traps it hides.

Enumerations {a|b|c}

Randomly selects one option from the list.

{option1|option2|option3}

Examples

{blue|grey|clear}
{|free|paid} plan                 ← empty option = sometimes nothing
{Acme {Pro|Lite}}                ← nested enumerations
{order {|#42-A} confirmed}       ← nesting with empty option

Rules

  • Delimiters: { and }
  • Separator: |
  • Supports nesting to arbitrary depth
  • Empty options are valid (produce empty string)
  • Resolution is from the innermost expression outward

Permutations [a|b|c]

Selects N elements, shuffles them, and joins with separators.

Simple permutations

All elements included, space-separated:

[1|2|3|4]

Output examples: 1 4 3 2, 2 3 4 1, 3 2 4 1

With separator

Uniform separator specified in < > at the start:

[<, > 1|2|3|4]

Output examples: 2, 1, 4, 3 · 4, 3, 2, 1

Important: No space between [ and <separator>.

Per-element separators

Each option can have its own separator defined with <sep> before the preceding |. The separator travels with its element during shuffle.

[<, > 1|2|3 < and >|4]

Output examples: 1, 3, 2 and 4 · 3, 1, 2 and 4

Auto-spacing: Word separators like <and> or <or> are automatically padded with spaces, so <and> produces  and . Punctuation separators (<,>) are not padded.

Permutations with combinations

Configurable min/max element count and separators:

[<minsize=1;maxsize=3;sep=", ";lastsep=" and "> apple|plum|orange|apricot]

Output examples: apple, plum and orange · apple and apricot · orange

Configuration parameters

ParameterDefaultDescription
minsizecount of allMinimum number of elements to pick
maxsizecount of allMaximum number of elements to pick
sep" " (space)Separator between non-final items
lastsepsame as sepSeparator before the last element

Permutation rules

  • Delimiters: [ and ]
  • Config block <...> must immediately follow [
  • Config parameters are semicolon-separated
  • String values in config are quoted: sep=", "
  • Enumerations and permutations can be nested inside options
  • HTML elements can be options
  • sep joins everything before the final pair, lastsep joins that pair: with two items selected only lastsep appears, with one neither does

Variables %var%

Defines a reusable variable that is substituted wherever it appears. Two directives declare one, and the choice is not cosmetic: #set is a macro — its value is substituted afresh at every reference, so any spintax inside it re-rolls; #def rolls the value once per render and hands that one result to every reference. (One render is one output; the same seed reproduces it.) While the value is a plain literal the two are identical; the difference appears the moment the value contains a choice.

#set %VARIABLE_NAME% = value or spintax structure

#def %VARIABLE_NAME% = value or spintax structure

Examples

#set %name% = John
#set %greeting% = {Hello|Hi|Hey}
#set %items% = [<minsize=2;maxsize=3;sep=", ";lastsep=" and "> apples|oranges|bananas]
Some text with %name% and %greeting%, also %items%.
And once more, %greeting% — a second reference.

/# %greeting% above may differ between the two references — #set re-rolls.
   #def picks once and keeps it: #/
#def %tone% = {friendly|warm|upbeat}
A %tone% intro, and a %tone% outro — always the same word.

Variable rules

  • #set and #def must start at the beginning of a line
  • Variable names are enclosed in %: %name%
  • Variable names are alphanumeric + underscore, and references are case-insensitive: %Tone% and %tone% are one variable
  • Values can contain any spintax syntax (enumerations, permutations, other variables)
  • #set is a macro: it expands when referenced, not when defined, and its value — including any spintax inside it — is substituted afresh and re-rolled at every reference
  • #def resolves its value once per render and holds that result everywhere. That is what keeps repeated words in agreement: a noun and the case forms built from it, a count feeding a {plural} block, any phrase whose repeats must match word for word
  • #def makes one variable consistent with itself; it does not correlate two variables. #def %Noun% and #def %NounGen% are two independent rolls and can land on different words — forms that must agree have to come from a single roll: one #def stem that every case form references, or synonyms that decline alike with the ending written outside the definition
  • A name is defined once. A second definition of the same name is reported as definition.duplicate-name and the render still goes ahead: between two of the same directive the later one wins, and when a #set and a #def share a name the #def wins whichever of them came first
  • A reference with no definition prints itself: %missing% stays in the output rather than disappearing
  • Neither directive crosses an #include: an included template does not see the parent's locals, and its own do not leak back. A rolled form reaches a child only as a runtime variable
  • #set and #def lines are stripped from output
  • Deep dive: see the Variables guide for scopes and the reroll gotcha, and Grammar-safe synonymization for case families

Variable scopes

A host can supply variables from more than one place. When the same name exists in several of them, the strongest wins:

  1. Runtime variables (strongest) — what the host passes into the render call: context in @spintax/core, shortcode attributes in the WordPress plugin: [spintax slug="greeting" name="Alice"]
  2. Local variables — defined with #set or #def inside the template
  3. Global variables (weakest) — host-wide defaults, such as the plugin's Settings page

Conditionals {?VAR?then|else}

Conditionals are the language's own construct — nothing like them existed in the GTW prototype. Where {a|b} is a uniform random pick that ignores variables, {?VAR?then|else} picks based on whether %VAR% has a value.

Use it for value-driven choices — show a free-tier line only when a free tier exists, render a pro-features block only when the user is on a paid plan, hide a CTA that does not apply.

The pre-pass runs before %var% expansion and before the random branch picker, so a falsy branch is fully discarded — nothing inside it is evaluated.

Forms

{?VAR?then}                ← truthy ⇒ then; falsy ⇒ empty
{?VAR?then|else}           ← truthy ⇒ then; falsy ⇒ else
{?!VAR?then|else}          ← inverted
{?HasFreeTier? — free tier available since %founded%|, trusted since %founded%}

Truthy and falsy

The rule is deliberately simpler than JavaScript — truthy = at least one non-whitespace character:

Value of %VAR%Truthy?
not declaredfalsy
empty stringfalsy
whitespace onlyfalsy
"0", "false"truthy (non-empty)
any other text or HTMLtruthy

Conditional rules

  • Variable names follow the same regex as %var% (case-insensitive)
  • The ! prefix inverts the check: {?!VAR?missing}
  • The first depth-0 | separates then from else; later top-level | stays literal in else
  • Nested conditionals evaluate outer-first — falsy branches short-circuit
  • Composite logic (&&, ||, comparisons) is not supported — pre-compute a guard variable in the assembler
  • Malformed forms ({??yes}, {?VAR}) never throw — the playground flags them as warnings
  • Deep dive: see the Conditional spintax guide for worked examples and anti-patterns

Plurals {plural %n%: language|languages}

Picks the grammatically correct word form for a number. The count goes before the colon, the forms after it, separated by |.

The form is chosen by the render locale, not by the template — so how many forms you must supply depends on that locale. English needs two, Russian needs three.

{plural %n%: form1|form2}          ← 2-form locale (en, de, es…)

{plural %n%: form1|form2|form3}    ← 3-form locale (ru, uk, sr…)
#def %LangCount% = 5
supports %LangCount% {plural %LangCount%: language|languages}
← supports 5 languages

Forms per locale

The locale is matched on its language subtag, so ru-RU and ru behave identically:

LocaleFormsSelected by
ru, uk, be, sr, hr, bs31 · 2–4 · 5 and up
every other locale, incl. en2exactly 1 · everything else

Supply the wrong number of forms and the engine reports plural.arity and leaves the block visible with fullwidth braces — a silent wrong plural never ships.

Plural rules

  • The opener is literal, including the space: {plural . {plural: x} and {pluralN: x} are not plural blocks
  • The colon is mandatory — it separates the count from the forms
  • The count is a %Var% reference or a literal integer; variables in the count are substituted before the form is chosen
  • Negative counts use the absolute value; 0 takes the "everything else" form
  • A count variable must be #def, not #set#set is a macro, so a value like {1|4|9} is still unresolved spintax when the plural is decided and the whole block renders empty. The playground flags this as plural.count-macro
  • A non-numeric or undefined count erases the block rather than guessing
  • Deep dive: see the Plural spintax guide for the Russian 3-form rules and worked examples

Includes #include

Embeds another template at the directive's position. #include is the one construct the engine cannot answer by itself: it keeps no template store, so the host supplies a resolver that turns a reference into template text. Where no resolver is installed — the playground and the MCP server on this site, both deliberately — the directive is inert and stays in the output as literal text.

#include "hero-text"

/# wrong: text before the directive on the same line leaves it literal #/
Intro: #include "hero-text"

Include rules

  • The directive must occupy the whole line. Leading whitespace is fine; text after the reference is not — Text #include "hero" stays literal
  • The reference is in double quotes; single quotes or no quotes are not the directive
  • Resolution belongs to the host: the WordPress plugin resolves by template slug or numeric ID, a JavaScript host passes an includeResolver
  • With no resolver the line stays literal in the output; when the resolver has no such template the line is removed instead — an unknown target costs you the block silently
  • Included templates can contain their own variables and spintax, and their own #include
  • Includes are resolved after the parent's enumerations and permutations have been picked, so an include inside the branch that won is embedded — one inside a discarded branch never happens
  • Chains work (a template includes a template that includes another); a template that includes itself, directly or through a cycle, is cut at the first repeat — with no error and no diagnostic
  • Child templates inherit global and runtime variables but not parent's #set / #def locals, and their own do not leak back
  • An include cannot be a definition's value: #def %x% = #include "y" is rejected as def.include-in-value
  • Before rendering, validate() flags an unknown target only when the host passes the list of known references; extract() returns the references a template needs, which is how a host prefetches them
  • Deep dive: see the Template composition guide for the assembler pattern most pipelines use instead

Comments /#...#/

Text between comment markers is stripped from the output before any other processing.

/#
  This is a comment section.
  It can span multiple lines.
  It won't appear in output.
#/

Comment rules

  • Start delimiter: /#
  • End delimiter: #/
  • Can span multiple lines
  • Cannot be nested
  • Removed before any other processing

Nesting

All syntax elements can be nested within each other to arbitrary depth:

{option1|[<, > sub1|sub2|sub3]|option3}

[<minsize=2;maxsize=3;sep=", ";lastsep=" and "> {red|blue} apples|{big|small} oranges|bananas]

#set %var% = {a|[b|c]}

Post-Processing

The engine applies automatic text correction after generation:

  1. Shield URLs, emails, domains, decimals, and abbreviations from capitalization
  2. Collapse duplicate spaces and tabs
  3. Remove whitespace before punctuation (, . ! ?)
  4. Add space after punctuation where missing
  5. Capitalize first letter of the output (skipping HTML tags)
  6. Capitalize after sentence-ending punctuation
  7. Capitalize after block-level HTML tags
  8. Capitalize after line breaks
  9. Restore shielded placeholders

Syntax Summary

FeatureSyntaxBehavior
Enumeration{a|b|c}Pick one random option
Permutation[a|b|c]Pick N, shuffle, join
Separator[<sep> a|b|c]Permutation with uniform separator
Per-element sep[<, > a|b <x>|c]Permutation with custom separators
Combinations[<config> a|b|c]Permutation with min/max count
Variable#set %var% = {a|b}Re-substituted at every reference — the spintax inside re-rolls
Variable (roll once)#def %var% = {a|b}One roll per render, held everywhere — how repeated words and their endings stay in agreement
Conditional{?VAR?then|else}Render then if truthy, else if falsy
Plural{plural %n%: language|languages}Agree the word form with the number, by locale
Include#include "slug"Embed another template — the host resolves the reference
Comment/#...#/Stripped from output

The language outgrew its prototype, Generating The Web (GTW) — templates written for GTW still run unchanged.