Zie619/n8n-workflows · error · Error
Quote not found
Error message
Quote not found
What it means
Thrown by the 'Prepare Overlay Text (Quote & Author)' Code node that builds an FFmpeg drawtext filter. It reads the quote text from the 'Merge File Selection Data' node via $node[...] and fails when json['Qoute'] is falsy. The misspelled key 'Qoute' is the actual field name used throughout this workflow, so a 'correctly' spelled Quote field will also trigger this error.
Source
Thrown at workflows/Code/1864_Code_Executecommand_Create_Webhook.json:434
"options": {
"append": false
},
"fileName": "music1.mp3",
"operation": "write"
},
"typeVersion": 1,
"notes": "This readWriteFile node performs automated tasks as part of the workflow."
},
{
"id": "9fdca64e-79d7-4a58-91dc-4aa9f9b3c4cc",
"name": "Prepare Overlay Text (Quote & Author)",
"type": "n8n-nodes-base.code",
"position": [
1620,
20
],
"parameters": {
"jsCode": "// Define separate configuration for the quote and the author\nconst quoteFont = \"Kanit-Italic.ttf\"; // Font for the quote\nconst quoteFontSize = 70;\nconst authorFont = \"Kanit-Italic.ttf\"; // Font for the author (ensure this supports Thai)\nconst authorFontSize = 50;\nconst fontColor = \"white\";\nconst lineHeightMultiplier = 1.1;\nconst videoWidth = 1080;\nconst margin = 40; // Gap from left and right edges\n\n// Effective width for the quote text (accounting for left/right margins)\nconst effectiveVideoWidth = videoWidth - 2 * margin;\n\n// Estimate average character width based on quoteFontSize (this is a rough estimate)\nconst avgCharWidth = quoteFontSize * 0.6;\nconst maxCharsPerLine = Math.floor(effectiveVideoWidth / avgCharWidth);\n\n// Retrieve the quote transcript and author from the \"Merge\" node\nconst transcript = $node[\"Merge File Selection Data\"].json[\"Qoute\"];\nif (!transcript) {\n throw new Error(\"Quote not found\");\n}\nconst author = $node[\"Merge File Selection Data\"].json[\"Author\"];\nif (!author) {\n throw new Error(\"Author not found\");\n}\n\n// Split the transcript into words and group them into lines based on maxCharsPerLine\nconst words = transcript.split(' ');\nconst lines = [];\nlet currentLine = \"\";\nlet currentCharCount = 0;\n\nwords.forEach(word => {\n const wordLength = word.length;\n const additionalSpace = currentLine ? 1 : 0;\n const potentialLength = currentCharCount + additionalSpace + wordLength;\n if (potentialLength <= maxCharsPerLine) {\n currentLine += (currentLine ? \" \" : \"\") + word;\n currentCharCount = potentialLength;\n } else {\n lines.push(currentLine);\n currentLine = word;\n currentCharCount = wordLength;\n }\n});\nif (currentLine) {\n lines.push(currentLine);\n}\n\n// Calculate layout for the quote block\nconst lineHeight = quoteFontSize * lineHeightMultiplier;\nconst totalHeight = lines.length * lineHeight;\n\n// Build drawtext commands for each quote line (centered horizontally)\n// Each line is positioned so that the entire quote block is vertically centered.\nconst quoteCommands = lines.map((line, index) => {\n // Escape any single quotes in the line\n const escapedLine = line.replace(/'/g, \"\\\\'\");\n return `drawtext=fontfile=${quoteFont}:text='${escapedLine}':fontsize=${quoteFontSize}:fontcolor=${fontColor}:x=(w-text_w)/2:y=((h-${totalHeight})/2)+(${index}*${lineHeight})`;\n});\n\n// Build the drawtext command for the author\n// Place the author text below the quote block with a small gap (e.g. 20 pixels)\n// Align it to the right by setting x = w - text_w - margin.\nconst authorY = `((h-${totalHeight})/2)+(${lines.length}*${lineHeight})+20`;\nconst escapedAuthor = author.replace(/'/g, \"\\\\'\");\nconst authorCommand = `drawtext=fontfile=${authorFont}:text='${escapedAuthor}':fontsize=${authorFontSize}:fontcolor=${fontColor}:x=w-text_w-${margin}:y=${authorY}`;\n\n// Combine all commands (separated by commas) into one drawtext filter string.\nconst fullDrawTextFilter = quoteCommands.concat(authorCommand).join(\", \");\n\n// Return the prepared filter string for insertion into your FFmpeg command.\nreturn {\n json: {\n drawText: fullDrawTextFilter\n }\n};\n"
},
"typeVersion": 2,
"notes": "This code node performs automated tasks as part of the workflow."
},
{
"id": "082cf794-89a9-42cc-b9ee-96792a17893f",
"name": "Generate Final Video Clip",
"type": "n8n-nodes-base.executeCommand",
"position": [
1640,
340
],
"parameters": {
"command": "=ffmpeg -i {{ $('Save Video Background Locally').item.json.fileName }} -i {{ $('Save Music Background Locally').item.json.fileName }} -filter_complex \"[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920[vid]; color=black@0.3:size=1080x1920:d=10[bg]; [vid][bg]overlay=shortest=1[bgvid]; [bgvid]{{ $json.drawText }}[outv]; [1:a]volume=0.8[aout]\" -map \"[outv]\" -map \"[aout]\" -aspect 9:16 -c:v libx264 -c:a aac -shortest output.mp4 -y"
},
"typeVersion": 1,
"notes": "This executeCommand node performs automated tasks as part of the workflow."
},View on GitHub (pinned to 94007c1445)
Solutions
- Confirm the upstream 'Merge File Selection Data' node output actually contains a first item with a non-empty 'Qoute' field (exact spelling).
- If the typo was corrected upstream, update this reader (and the selection node in error 49) to 'Quote' consistently.
- Use $('Merge File Selection Data').first().json instead of the legacy $node accessor for predictable item resolution, or read from the current item if the data flows in via connections.
- Validate the quote branch earlier (at selection time) so the run fails with error 49's clearer message before FFmpeg setup begins.
Example fix
// before
const transcript = $node["Merge File Selection Data"].json["Qoute"];
if (!transcript) {
throw new Error("Quote not found");
}
// after
const merged = $('Merge File Selection Data').first().json;
const transcript = (merged["Qoute"] ?? merged["Quote"] ?? '').toString().trim();
if (!transcript) {
throw new Error(`Quote not found. Merge node payload keys: ${Object.keys(merged).join(', ')}`);
} Defensive patterns
Strategy: type-guard
Validate before calling
const merged = $('Merge File Selection Data').first().json;
const transcript = (merged["Qoute"] ?? merged["Quote"] ?? '').toString().trim();
const ready = transcript.length > 0; Type guard
function hasQuote(merged) {
return Boolean((merged?.["Qoute"] ?? merged?.["Quote"] ?? '').toString().trim());
} Prevention
- Use $('NodeName').first().json rather than legacy $node access for predictable item resolution.
- Accept both spellings (Qoute/Quote) during any typo-migration period.
- Validate quote presence at the selection node (error 49) before FFmpeg work starts.
- Include the available keys in the error message for instant field-name debugging.
When it happens
Trigger: $node['Merge File Selection Data'].json has no 'Qoute' key, or its value is empty string/null/undefined. Causes: the merge node didn't receive quote items (see error 49), the field is spelled 'Quote' in the data, or the value is blank in the source sheet. Note $node[...] returns the node's first output item only — if the quote sat on a later item, it is invisible here.
Common situations: Upstream quote source empty or filtered out; someone 'fixed' the Qoute typo upstream but not in this reader; the merge node emitted the quote on a non-first item; blank cell in the quotes spreadsheet.
Related errors
- Author not found
- Quote not found
- Approved quantity must be greater than 0
- ${fileName} → ${baseName} → Unrecognized file name structure
- The video ID parameter is empty.
AI-assisted analysis of Zie619/n8n-workflows@94007c1445 (2026-08-15).
Data as JSON: /api/errors/eec3bb933cbe0023.
Report an issue: GitHub.