Spintax 用于 n8n:每个线索、每个商品,都是独一无二的文案

在 n8n 里,你的线索列表或商品数据源本来就是一行行的数据。n8n-nodes-spintax 添加一个节点,把每一行变成成品文案:每个线索一封冷邮件,每个店面一份商品描述——同一个流程,换个数据源而已。本指南手把手带你走完:安装、第一次渲染、接入外发、商品数据源变体,以及让你自己的 LLM 撰写模板的循环。

一个流程,两种数据源

下面的一切都建立在一条规则上:传入行的每个顶层字符串、数字或布尔字段,本身就是一个 %variable%——你什么都不用映射。给流程喂一份线索列表,它就为每个线索写出主题和正文;喂一份商品数据源,它就为每个商品写出描述。模板才是资产,数据源随便换。

线索列表Spintax——按行渲染商品数据源你的发送工具你的店面
一个流程,两种数据源:每一行都渲染成成品文案

安装节点

在你的 n8n 里打开 Settings → Community Nodes → Install,输入 n8n-nodes-spintax,确认。安装到此结束:@spintax/core 引擎已打包进节点,不需要任何凭据,也从不向你的实例之外发起请求。(社区节点能否安装取决于实例管理员的设置;n8n Cloud 的节点列表只显示经过验证的节点。)

想直接从一块能跑的画布开始?通过 ⋯ → Import from URL… 导入现成工作流——节点的仓库里附带两个,发布前都在真实的 n8n 里端到端验证过:

  • Cold-email bridge ——输入线索(或商品数据源),输出每行独一无二的主题和正文。
  • AI authoring funnel ——模板由你的 LLM 起草,流程负责校验、修复和渲染。

第一次渲染

  1. 添加一个 Spintax 节点。操作默认就是 Render
  2. 把模板贴进 Template 字段:
{Quick|One|Small} {question|idea} for %company% — the %product% {listing|page}
  1. 用带 companyproduct 字段的行跑一遍——Google 表格、CRM 导出,任何能产出行的东西都行。

每一行出来都带一个 rendered 字段(可在 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"

这两行结果不同,是因为 Seed 字段里放了表达式 {{ $json.company }}:subject:每一行抽取属于自己的变体,而且变体是稳定的——重跑工作流,谁的文案都不会被重新洗牌,你随时能拿出某个线索当初收到的原文。想让所有人重抽,改一下后缀即可。Seed 留空则每次运行都是全新抽取。

接入你的外发流程

为什么要在 n8n 里渲染?因为你的发送平台不会替你做:Instantly、Smartlead 这些平台顶多解析扁平的 {a|b|c},而排列、条件和数量一致把一封实测的五行邮件从 59,049 种组合提升到 850 万种。所以先渲染,再发送成品文本。这座桥只需三个节点:

  1. 你的线索来源——Google Sheets、Airtable、CRM 导出。
  2. 两个 Render 节点:一个写 subject,一个写 body,各自带上面那样的按行 seed。
  3. 你的发送工具自己的 n8n 节点——把 subjectbody 映射进去,完成。

上面列出的 cold-email bridge 工作流就是这套东西的成品:把它的示例 Code 节点换成你的真实数据源即可。

同一流程,换成商品数据源

现在把线索列表换成商品数据源。其他什么都不用改——但让模板用上商品目录真正拥有的字段。贴这个:

{?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: (空)
→ "The cast-iron skillet set deserves distinct copy per storefront — 1 unit in stock."

读一读发生了什么:促销那句话只出现在 has_discount 有值的行里,而 "1 unit" 永远不会写成 "1 units"。你既没搭 if 节点,也没做语法检查——条件数量形式 就长在模板里。中文模板没有复数变形,用不到这一项;但如果模板是俄语等有复数变形的语言,请在 Render 节点把 Locale 设成对应语言——英文模板用默认值即可。

同一份商品目录卖到多个店面?每个站点跑一遍数据源,把站点放进 seed:先 {{ $json.sku }}:site-a,再 :site-b。每个店面都会得到每条描述属于自己的稳定变体——同样的数据,处处不同的文本,对重复内容惩罚的防御就此变成机械动作。为什么模板化的变体胜过“让模型把每一页都写一遍”:见 什么是 Spintax?;那条路的成本:见 AI 内容的真实成本

让你的 LLM 来写模板

手写一个丰富的模板,正是大家跳过的那一步——那就别写。导入 AI authoring funnel 工作流,只需接一样东西:模型节点上的凭据;或者把那个节点换成你已经在用的任何 LLM。Spintax 节点不绑定任何提供商:它们递给模型的是纯文本 systemPrompt/userPrompt,读回来的也是纯文本。

ValidInvalid简报Build Authoring Prompt你的 LLM 节点ValidateRender / Render ManyBuild Repair PromptLLM——修复Email · Telegram · CRM
创作漏斗——现成工作流把修复循环限制在一轮

填入你的简报(“给 %company% 的 %first_name% 写一封简短的外联邮件……”),列出允许模型使用的变量——屈折语言还可以为每个变量标注语法格——然后运行。Build Authoring Prompt 会生成整个 spintax 生态共用的标准提示词(版本号在 promptVersion 里)。Validate 把草稿分流到 Valid 或 Invalid 输出;走到 Invalid 时,Build Repair Prompt 会把出错的具体行列指给你的模型并送回去——工作流把这个循环限制在一轮,通常就够了。走到 Valid 时,Render Many 给你五个变体供审读。

运行前值得了解的三个设置:

  • Clean Model Output ——在 Validate 上打开。不管提示词怎么写,模型都爱把回复裹进代码围栏;这个开关会把它们剥掉、存进 cleanedTemplate,之后每条诊断的位置都精确指向这段文本。
  • Locale ——只在 Build Authoring Prompt 里设一次。它会作为 spintaxMeta 随行数据传递,后面的每个节点都从那里读取:Validate、Render 和 Render Many 的 Locale 字段留空即可。(Validate 会把 spintaxMeta 透传到自己的两个输出上;你的 LLM 节点会丢弃陌生字段——链条正因如此不断。)
  • Render Many 的 Count(默认 5,最多 100,另有 Max Attempts 尝试预算,0 表示自动)——并在输出里对照 producedrequested。不同的 seed 是相互独立的抽取,不保证结果各不相同:变化空间小的模板可能根本给不出五个变体——{Fast|Quick} delivery 无论要多少个都只有两种——节点会直说,而不是无限重试或悄悄少给。
  • Base Seed——如果你要把生成的内容存下来。设了它以后,每个变体都会在 attemptSeed 字段里带上真正生成它的那个 seed:把这个字段和正文一起保存。它不是行号——一旦两次抽取撞在一起,尝试计数和数组位置就此分道扬镳,而这个 seed 是日后唯一能把那一份文档重建出来的东西。

检查真正产出的东西

Validate 判的是模板,判不了渲染结果——而一个挑不出毛病的模板,照样会时不时冒出一行坏文案,因为缺陷长在选择的组合里,而不在源码里:相邻两个槽位选中了同一个词,一个槽位的名词碰上另一个槽位的代词,一次不巧的拼接在逗号前留下了空格。模板没错;错的是五十行里的某一行——而五十行已经没人会逐行重读了。在渲染之后加一个 Lint 节点:有缺陷的行从它的第二个输出离开,干净的继续往下走。把它指向模板而不是某一行渲染结果,它会自己抽一批样本,然后告诉你有多少份文档是干净的——这就是用来打磨槽位的那个数字,在正式跑之前,而不是之后。

还有一个关于整批产出的问题,是按字符串精确去重回答不了的:这些文档真的各不相同,还是同一副骨架换了五十顶帽子?Uniqueness 把所有传入的行当作一个池子来读,剔掉近似重复项,并给出一个足迹(footprint)——池子里重复出现的五词窗口所占的比例。在同等规模的真实池子上测得:一个模板约 0.96,六个模板约 0.02;反直觉的地方在于,要更多同一模板的变体没有用,因为骨架由模板固定,重渲染稀释不了任何东西。真正能把这个数字压下来的,只有新模板——或者在现有模板里做更密的变化。还有一个设置决定了这个数字是否有意义:把每一行都重复的字符串(商品名、merge 标签)填进 Shared Strings,否则这项指标量到的是你的商品名,而不是你的文案。

当文案离开你之后还有别人展开它

有时渲染并不是最后一步:文案还要送去 Mailchimp 的 merge 标签、Liquid、你自家 CRM 的宏。那些语法和这里的会撞车——%name% 在这边是变量,在那边是宏;方括号是排列语法,所以带方括号的宏出来就没了括号;而排版美化那一步会毫不客气地在宏的参数里塞进一个空格。把渲染包进 Protect Placeholders:Protect 模式把外来字符串换成渲染碰不到的标记,Restore 模式把它们逐字节放回去并校验整个来回。一旦对不上,它就大声拒绝——包括那个看上去毫无异样的陷阱:你的变量取了人家宏的名字,我们的引擎在对方看到之前就把它展开了,留给你一份看着合理、其实不对的文档。

无需你操心的部分

抓取来的数据默认是安全的:含 {| 的公司名会按文本渲染,绝不会被当成标记——传入值在模板看到它们之前,就已经过了引擎的防护。你亲手在节点里输入的变量是受信任的,所以刻意写的 {Mr|Ms} 照常工作;每一条都有自己的防护开关,以备哪天你往里粘贴了外部数据。而且这里没有任何东西会“回家汇报”:没有凭据、没有网络调用、没有文件系统——渲染就发生在你的实例上。

正式跑之前

  • Seed 是按行的表达式,重跑不会把已发出的文案重新洗牌。
  • Locale 已设置——在 Render 节点上,或在漏斗的 Build Authoring Prompt 里设一次(英文模板用默认值即可)。
  • Render Many 的 produced 等于 requested ——或者你清楚为什么不等。
  • 你读过二十行渲染结果,而不是一行——或者由一个 Lint 节点替你把它们全读了。
  • 如果你产出的是一个池子,而不是每个线索一行——Uniqueness 确认它确实多样,而不只是没有重复。

模板带去任何运行时

你在这里搭好的模板按契约可以带走:同样的语法、同样的校验结论、同样的数量语义,出现在 JavaScriptPHP、Python 和 Object Pascal 四个引擎 里,由一套共享一致性语料库共同约束。去演练场EN测试它,用 Spintax Studio 批量导出,在 WordPress 里渲染——资产是模板,引擎只是可替换的运行时。在你的发送平台或电商平台原生支持这套语法之前,这个节点就是通往它的无代码路径。

完整语法构件

排列 管顺序和长度。条件 管随数据变化的句子。数量一致 管可数名词。还有创作漏斗所自动化的AI 到模板工作流