Conditional spintax: {?VAR?then|else}

Sometimes the choice between two phrasings is not a coin flip — it depends on a fact. The casino has crypto, or it doesn't. The product has a free tier, or it doesn't. Conditional spintax is the value-driven counterpart to {a|b} enum branches: instead of picking randomly, it picks based on whether a variable is truthy.

The three forms

Conditional syntax adds three new tokens on top of the GTW spintax family. All three are evaluated before enums and permutations resolve — they are a pre-pass, not a runtime branch.

FormMeaning
{?VAR?then} Render then if %VAR% is truthy; render nothing otherwise.
{?VAR?then|else} Render then if truthy; render else if falsy.
{?!VAR?then[|else]} Inverted: render then when %VAR% is falsy.

The ! prefix flips the check. There is no separate {?VAR??else} form — if you only want the falsy branch, write {?!VAR?else}.

Truthy and falsy

The truthy rule is deliberately simpler than JavaScript's. You should not have to remember coercion edge cases.

Value of %VAR%Truthy?
not declared at allfalsy
empty string ""falsy
whitespace only (spaces, tabs, newlines)falsy
"0"truthy (string zero is non-empty)
"false"truthy (still a non-empty string)
"x", "<p>…</p>", raw spintaxtruthy

That is the whole rule. Truthy = at least one non-whitespace character. If you ever need a JavaScript-flavoured truthiness, do that check in the assembler before passing the variable in.

Truthiness reads the raw value

The lookup is on the raw stored value of the variable. Nested %var% references inside that value are not expanded for the truthy check — only later, when expandVariables runs as a separate stage.

#set %X% = %Other%
{?X?yes|no}     → "yes"

Even if %Other% would expand to an empty string, %X%'s raw value is the literal string %Other% — which is non-empty, hence truthy. To get value-aware truthiness, write %X% as a clean '1' or '' directly. Compute the guard in the assembler before passing it in.

The two-pass pipeline

The engine processes a template in stages. Conditionals get two evaluation passes:

1. strip comments
2. extract #set directives
3. merge variables
4. apply conditionals       ← pass 1
5. expand %var% references
6. apply conditionals       ← pass 2
7. resolve enumerations
8. resolve permutations
9. post-process

Pass 1 handles conditionals authored directly in the template body. It runs before variable expansion so a falsy branch is discarded without wasting cycles on its %var%s.

Pass 2 handles the case where a variable's value itself contains a conditional:

#set %CTA% = {?HasBonus?Claim bonus|Deposit now}
%CTA%                  /# pass 2 sees the conditional after expansion #/

One clarification: pass 2 does not loop. If the chosen branch contains a fresh %var% reference, that reference is left literal — there is no third expansion pass. For most real templates this is fine; the guards used in conditional branches (%HasCrypto%, %HasFiat%) are typically the same ones already resolved in pass 1.

Worked example: banking blocks

This is the case the syntax was designed for. A casino review template renders a "Deposits and withdrawals" section. Some casinos accept crypto, some accept fiat, some accept both, a few accept neither.

The variable assembler computes two guards plus three pre-rendered HTML chunks:

%CasinoHasCrypto%    "1" or ""
%CasinoHasFiat%      "1" or ""
%CasinoCryptoSection%   /# already-rendered <h3> + <ul> #/
%CasinoFiatSection%
%CasinoLimitsSection%

The orchestrator template uses conditionals to gate the optional fallback line for casinos with no payment data:

%CasinoFiatSection%%CasinoCryptoSection%%CasinoLimitsSection%
{?!CasinoHasCrypto?{?!CasinoHasFiat?<p>Banking details will be published shortly.</p>}}

Read the bottom line as: if not crypto and not fiat, render the fallback paragraph. Nested conditionals short-circuit outer-first, so when %CasinoHasCrypto% = "1" the outer falsy check fails immediately and the inner conditional is never evaluated.

Compare with the old workaround — a guard variable filled by spintax that picks between an empty string and a fallback paragraph at random with weighted probability. That worked, but the rendered output was non-deterministic and the template had to thread an extra variable through. Conditional syntax says exactly what it means.

Pick vs conditional — do not confuse them

Anti-patterns

1. Boolean operators

There is no &&, ||, !=, or == in conditional spintax. The syntax is intentionally minimal. If you need composite logic, compute the boolean in the variable assembler:

/# wrong: not supported #/
{?HasCrypto && HasLicense?…}

/# right: compose in the assembler #/
#set %ShowCryptoBlock% = {?HasCrypto?{?HasLicense?1}}
{?ShowCryptoBlock?…}

The assembler can use any host language and any logic. Spintax stays a templating tool, not a programming language.

2. #set inside a branch

#set directives are extracted before any conditional pass — that is step 2 of the pipeline. A #set line inside a {?…?…} branch fires unconditionally; the conditional only controls whether the empty leftover of the line stays in the chosen branch's text.

/# Both #set lines fire. The second wins. #/
{?A?
#set %x% = first
|}{?A?
#set %x% = second
|}%x%
→ always "second", regardless of A

If you need conditional assignment, do it in the assembler.

3. Top-level | as literal in then

The first depth-0 | inside the body separates then from else. Any further | at depth 0 stays literal — but in the else branch, not the then branch.

{?A?x|y|z}     /# A truthy → "x"; A falsy → "y|z" #/

If the then branch needs a literal |, wrap it in nested braces or use the HTML entity &#124;:

{?A?{x|y}}             /# inner | is depth 1, not a separator #/
{?A?x &#124; y}         /# explicit entity, renders as "x | y" #/

4. Confusing pre-pass and runtime

Conditionals run as a pre-pass before enums and permutations resolve. That means a falsy branch is completely discarded — no random choice from it ever fires. If your template has a %RandomQuirk% permutation deep inside the falsy branch, that permutation is never evaluated when the conditional is falsy. Good. That is the point.

Lenient parsing — malformed forms are not fatal

Bare ? in prose ("How? Like this?") is common. The parser is deliberately lenient: any {?… that does not match the grammar is left literal rather than throwing.

The validator on this site (and the playground) does flag balanced-malformed shapes as warnings so you catch them in the editor:

FormTreatment
{?VAR?then — no closing }warning: unmatched open brace (same as any other {)
{??yes} — empty namewarning: malformed conditional (empty name)
{?VAR} — missing ? separatorwarning: malformed conditional (missing separator)
How? Like this?plain prose, no warning

At runtime, every malformed form is non-throwing. The engine continues into the normal enum/permutation passes; balanced malformed tokens may end up consumed by later stages, so do not rely on literal preservation.

Quick checklist

  • Use {?VAR?…} when the choice depends on a value, never {a|b}.
  • Use {?!VAR?…} instead of inventing a separate "no" guard variable.
  • Compute composite booleans in the assembler. Spintax conditionals are atomic.
  • Never put #set inside a branch — it fires unconditionally.
  • If then needs a literal |, wrap it in {…} or use &#124;.
  • Truthy = non-whitespace, full stop. Pre-compute special truthiness in the assembler.
  • Pre-render HTML fragments into variables for sections that change shape per tenant. Conditionals gate them.

Try it live

The playground ships with a {?HasFreeTier?…|…} example in the default template. Toggle %HasFreeTier% between 1 and empty to see both branches without typing anything else.


Continue the series