# Spintax for n8n: unique copy per lead — and per product n8n is where your lead list or product feed already lives as rows. [`n8n-nodes-spintax`](https://www.npmjs.com/package/n8n-nodes-spintax) adds one node that turns each row into finished text: a cold email per lead, a product description per storefront — same flow, different feed. This guide walks you through it: install, your first render, the lead bridge, the product variant, and the loop where your own LLM writes the template. ## One flow, two feeds Everything below rests on one rule: every top-level string, number or boolean field of an incoming item is already a `%variable%` — you map nothing. Feed the flow a lead list and it writes a subject and body per lead. Feed it a product feed and it writes a description per product. The template is the asset; the feed is interchangeable. One flow, two feeds: every row renders to finished text ## Install the node In your n8n, open **Settings → Community Nodes → Install**, enter `n8n-nodes-spintax`, confirm. That is the whole setup: the node bundles the [`@spintax/core`](https://spintax.net/spintax-for-javascript.md) engine, asks for no credentials, and never calls out of your instance. (Community nodes install where the instance admin allows them; on n8n Cloud the picker carries verified nodes.) If you would rather start from a working canvas, import a ready-made workflow via **⋯ → Import from URL…** — two accompany the node in its repository, both verified against a live n8n before release: - [**Cold-email bridge**](https://raw.githubusercontent.com/investblog/spintax-js/main/packages/n8n-node/templates/cold-email-bridge.json) — leads (or a product feed) in, a unique subject and body per row out. - [**AI authoring funnel**](https://raw.githubusercontent.com/investblog/spintax-js/main/packages/n8n-node/templates/ai-authoring-funnel.json) — your LLM drafts the template; the flow validates, repairs and renders it. ## Your first render 1. Add a **Spintax** node. The operation is already **Render**. 2. Paste a template into the Template field: ``` {Quick|One|Small} {question|idea} for %company% — the %product% {listing|page} ``` 3. Run it on items that carry `company` and `product` fields — a Google Sheet, a CRM export, anything that produces rows. Each item comes out with a `rendered` field (rename it in **Output Field**): ``` Brightline Gear → "Small idea for Brightline Gear — the Trekker 45L backpack page" Nordic Home → "Quick question for Nordic Home — the cast-iron skillet set page" ``` Those two came out different because the **Seed** field held an expression, `{{ $json.company }}:subject`: each row draws its own variant, and the variant is stable — re-run the workflow and nobody's copy reshuffles, so you can always show what a given lead received. Change the suffix when you want to re-roll everyone. Leave Seed empty for a fresh draw per run. ## Wire it into your outreach Why render inside n8n at all? Because your sending platform will not do it: [Instantly, Smartlead and the rest parse flat `{a|b|c}` at best](https://spintax.net/spintax-for-cold-email.md), while permutations, conditionals and plural agreement take one measured five-line email from 59,049 combinations to 8.5 million. So render first, send finished text. The bridge is three nodes: 1. Your lead source — Google Sheets, Airtable, a CRM export. 2. Two **Render** nodes: one writes `subject`, one writes `body`, each with a per-row seed as above. 3. Your sending tool's own n8n node — map `subject` and `body` into it, done. The cold-email bridge workflow above is exactly this, ready to import; swap its sample Code node for your real source. ## Same flow, product feed Now swap the lead list for a product feed. Change nothing else — but let the template use the fields a catalogue actually has. Paste this one: ``` {?has_discount?With the {current|running} promo at %price%, the|The} %product% {deserves|warrants} {fresh|distinct} copy per {storefront|channel} — %in_stock% {plural %in_stock%: unit|units} in stock. in_stock: 14, has_discount: yes → "With the current promo at $129, the Trekker 45L backpack deserves distinct copy per storefront — 14 units in stock." in_stock: 1, has_discount: (empty) → "The cast-iron skillet set deserves distinct copy per storefront — 1 unit in stock." ``` Read what happened: the promo sentence exists only for rows where `has_discount` is set, and "1 unit" never comes out as "1 units". You wrote neither an if-node nor a grammar check — the [conditional](https://spintax.net/docs/conditional-spintax.md) and the [plural form](https://spintax.net/docs/plural-spintax.md) are part of the template. Selling the same catalogue on several storefronts? Run the feed once per site and put the site into the seed: `{{ $json.sku }}:site-a`, then `:site-b`. Each storefront gets its own stable variant of every description — same data, distinct text everywhere, which is the defence against duplicate-content penalties made mechanical. Why templated variation beats letting a model write every page: [What is spintax?](https://spintax.net/what-is-spintax.md); what the model route costs: [What AI content actually costs](https://spintax.net/ai-content-costs.md). ## Let your LLM write the template Writing a rich template by hand is the part people skip — so don't. Import the AI authoring funnel workflow and connect exactly one thing: credentials on its model node, or swap that node for any LLM you already run. The Spintax nodes are provider-agnostic: they hand your model plain `systemPrompt`/`userPrompt` text and read plain text back. The authoring funnel — the ready-made workflow caps the loop at one repair round Fill in your brief ("a short outreach email for %first\_name% at %company%…"), list the variables the model may use — with [a grammatical case per variable](https://spintax.net/docs/grammar-safe-spintax.md#case-families) if you write in an inflected language — and run. **Build Authoring Prompt** emits the canonical prompt the whole spintax ecosystem shares (versioned in `promptVersion`). **Validate** routes the draft to its Valid or Invalid output; on Invalid, **Build Repair Prompt** points your model at the exact offending line and column and sends it back — the workflow caps that loop at one round, which is usually enough. On Valid, **Render Many** gives you five variants to read. Three settings worth knowing before you run it: - **Clean Model Output** — turn it on for Validate. Models wrap replies in code fences no matter what the prompt says; this strips them into `cleanedTemplate`, and every diagnostic position points into exactly that text. - **Locale** — set it once, in Build Authoring Prompt. It rides the item as `spintaxMeta`, and every later node reads it from there: leave the Locale fields of Validate, Render and Render Many empty. (Validate passes `spintaxMeta` through on both outputs; your LLM node drops unknown fields, which is exactly why the chain stays unbroken.) - **Count** on Render Many (default 5, up to 100, plus a Max Attempts budget where 0 means automatic) — and read `produced` next to `requested` in the output. Distinct seeds are independent draws, not distinct results: a low-variety template may simply not have five variants to give — `{Fast|Quick} delivery` yields two however many you request — and the node tells you so instead of retrying forever or silently under-delivering. - **Base Seed**, if you keep what you generate. Every variant then carries the seed that produced it in `attemptSeed` — save that field with the text. It is not the row number: as soon as two draws collide, the counter and the position part ways, and the seed is the only thing that rebuilds that one document later. ## Check what actually came out Validate judges the template. It cannot judge the render — and a flawless template still turns out the odd broken line, because that defect lives in the _combination_ of choices rather than in the source: two neighbouring slots pick the same word, a noun from one slot meets a pronoun from another, an unlucky join leaves a space before a comma. Nothing is wrong in the template; something is wrong in one row out of fifty, and fifty rows is already more than anyone re-reads. Add a **Lint** node after your render and the defective rows leave on its second output while the clean ones carry on. Point it at a template instead of a rendered row and it draws a sample itself, then tells you how many documents came out clean — that is the number to fix slots against, before the real run rather than after it. One limit is worth knowing before you rely on it. Judging a repeated word means knowing which words a language repeats by construction — its prepositions, articles and conjunctions — and the node ships that vocabulary for five template languages: English, Russian, German, Spanish and Portuguese. Write your template in another one and the repetition check stays off rather than reporting every preposition as a defect; paste that language's function words into **Function Words** and it runs. Either way the node says which checks it skipped, in `skipped` — a rule that never ran is not the same as a rule that found nothing, and only one of those means your text is clean. Then there is the question about the batch as a whole, the one exact-string dedupe cannot answer: are these documents actually different, or is it one skeleton wearing fifty hats? **Uniqueness** reads every incoming item as one pool, drops the near-duplicates and reports a _footprint_ — the share of the pool's five-word windows that repeat across it. Measured on real pools of the same size, one template scores about 0.96 and six templates about 0.02; the counter-intuitive part is that asking for more variants of the same template cannot move it, because the skeleton is fixed by the template and re-rendering dilutes nothing. Only new templates — or denser variation inside the one you have — bring the number down. And one setting decides whether the number means anything at all: put the strings every row repeats (a product name, a merge tag) into **Shared Strings**, or the metric ends up measuring your product name instead of your writing. ## When something else expands the text after you Sometimes the render is not the last step: the copy goes on to Mailchimp merge tags, to Liquid, to your CRM's own macros. Those syntaxes collide with this one — `%name%` is a variable here and a macro there, square brackets are permutation syntax so a bracketed macro comes out without them, and the cosmetic pass will happily put a space inside a macro parameter. Wrap the render in **Protect Placeholders**: in Protect mode it swaps the foreign strings for markers the render cannot touch, in Restore mode it puts them back byte-exact and checks the round trip. It refuses loudly when something does not add up — including the trap that looks like nothing at all, a variable of yours named like a macro of theirs, which our engine expands before the other one ever sees it and leaves you with a plausible, wrong document. When you write the template yourself, you can mark the foreign fragment in place instead: `/!{{ first_name }}!/` or `/![Docs](https://example.com)!/` comes out exactly as written, with no spintax read inside it and no cosmetic pass applied to it. The node supports this [verbatim span](https://spintax.net/docs/syntax.md#verbatim) from version 0.3.7. Protect Placeholders is still the tool for fragments the template does not mark, such as those arriving with the data. ## What you don't have to babysit Scraped data is safe by default: a company name containing `{` or `|` renders as text, never as markup — incoming values pass through the engine's shield before the template sees them. Variables you type into the node yourself are trusted, so a deliberate `{Mr|Ms}` keeps working; each entry has its own shield toggle for the day you paste external data into one. And nothing here phones home: no credentials, no network calls, no filesystem — the render happens on your instance. ## Before the real run - Seed is a per-row expression, so a re-run cannot reshuffle sent copy. - Locale is set — on the Render node, or once in Build Authoring Prompt for the funnel. - `produced` equals `requested` on Render Many — or you know why not. - You read twenty rendered rows, not one — or a **Lint** node read all of them for you. - If what you generated is a pool rather than one row per lead, **Uniqueness** says it is varied, not merely free of duplicates. ## Take the template anywhere The template you built here is portable by contract: the same syntax, validation verdicts and plural semantics ship in the [JavaScript](https://spintax.net/spintax-for-javascript.md), [PHP](https://spintax.net/spintax-for-php.md), Python and Object Pascal [engines](https://spintax.net/spintax-engines.md), held to one shared conformance corpus. Test it in the [playground](https://spintax.net/play/), batch-export from [Spintax Studio](https://spintax.net/spintax-editor.md#studio), render it in WordPress — the template is the asset, engines are interchangeable runtimes. And until your sending or e-commerce platform adopts the syntax natively, this node _is_ the no-code route to running it. ## The constructs, in full [Permutations](https://spintax.net/docs/permutations.md) for order and length. [Conditionals](https://spintax.net/docs/conditional-spintax.md) for data-driven lines. [Plural agreement](https://spintax.net/docs/plural-spintax.md) for counted nouns. And [the AI-to-template workflow](https://spintax.net/ai-spintax-templates.md) the authoring funnel automates. --- [Open in playground](https://spintax.net/play/) [Back to all guides](https://spintax.net/docs.md) --- Source: https://spintax.net/spintax-for-n8n/