The symptom that got noticed first wasn't the real bug
What actually got flagged was cosmetic: a live post had hashtags awkwardly mixed into the middle of a sentence, plus a second batch of hashtags at the end. Chasing that literally — as a hashtag-placement bug — would have missed the real story. The post itself, on every affected platform, wasn't the intended per-platform copy at all. It was a generic fallback string, and the hashtag mess was just a visible side effect of that fallback firing.
Where the fallback text actually came from
The workflow's design has an LLM generate a single JSON object holding differently-worded copy for each destination platform, which a Code node then parses and distributes. That Code node has a catch block for exactly the case where the LLM's response can't be parsed as JSON — a reasonable safety net on paper, meant to produce something rather than nothing. Its actual fallback was a bare project.title + ': ' + project.description, truncated, with a hashtag string concatenated directly onto the end. That fallback fired on this run, for every platform at once, because the parse step it was guarding against had genuinely failed.
Why the JSON failed to parse
The specific failure was Bad control character in string literal — the standard error JSON.parse throws when a string value contains a raw, unescaped control character, most commonly a literal newline where the JSON spec requires the escaped two-character sequence \n instead. Groq's response that evening had exactly that: an actual newline byte sitting inside one of the string fields, rather than its properly escaped form. Perfectly valid as a plain-text sentence, invalid as JSON.
Two separate mechanisms stacking a second hashtag block
The visible symptom had two contributing pieces, not one. First, the fallback's own hashtag suffix was glued directly onto the sentence with no line break, which alone would read oddly. Second, a separate safety-net function elsewhere in the same Code node checked only how many hashtags a piece of text already contained — if the count was below a threshold, it appended its own hashtag line at the end, with no awareness of whether hashtags already existed inline, earlier in the same text. Both mechanisms were individually reasonable; combined, they guaranteed a duplicate hashtag block on exactly the kind of text the fallback was producing.
The fix, in two parts
The immediate parse failure was fixed at the source: sanitizing raw control characters out of the LLM's response text — escaping stray literal newlines, carriage returns, and tabs to their proper JSON string-escape form — before handing it to JSON.parse, so this specific class of otherwise-valid LLM output stops tripping the catch block at all. The hashtag duplication was fixed independently, by replacing the count-based safety net with logic that strips every hashtag out of a body of text first, deduplicates them, and rebuilds exactly one trailing hashtag line — a fix that holds regardless of whether the text came from a clean parse or from the fallback path, closing off the duplication risk structurally rather than patching one specific trigger for it.
What made this worth writing up
Neither the prompt nor the platforms were the problem — an LLM producing a stray, unescaped control character inside otherwise coherent output is common enough that any pipeline parsing structured LLM output as JSON should assume it will eventually happen, rather than treating a parse failure as a rare edge case. The second lesson is just as generalizable: a fallback path meant to degrade gracefully is still real production output the moment it fires, and deserves the same scrutiny as the primary path — including checking that its own text-formatting logic (hashtags, truncation, whatever else) can't misbehave in ways the primary path never would.
n8n Automation Hub