WFDASH-WEBAPP/n8n_workfows/WFDASH — Quote Outbound Send (with-outbound-attachments).json
Dave 3d009b144f fix(quotes): persist letterhead choice on terminal-status quotes too
The first letterhead-persistence fix (1e3121d) gated the WHOLE PB
write for generateQuotePdf's operator-choice persistence behind
"quote.status is not terminal" -- because terminal statuses
(approved/emailed/complete/cancelled/sending) deliberately skip the
pdf_generated_at re-stamp.

But regenerating a PDF on an already-terminal quote (e.g. clicking
"ISherwood" on an already-`emailed` quote from the process page) is
a normal, supported operator action -- confirmed live: it renders
the right letterhead and toasts "quote kept at emailed". That regen
hit the terminal branch and skipped the PB write entirely, so
company_letterhead never updated. A later auto-regen (sendQuoteEmail's
staleness check, e.g. triggered by a markup edit) then fell back to
the stale stored value and shipped the wrong letterhead again --
reproducing the original bug through a path the first fix didn't
cover.

Verified against the live quote via a direct PocketBase read
(bypassing app/MCP caching): company_letterhead was unchanged by the
first fix's terminal-status regen, and is now correctly persisted
while pdf_generated_at/status stay untouched as intended.

Split the letterhead write out from the timestamp/status write so it
always persists on an explicit operator choice regardless of quote
status. Added a regression test for the terminal-status + explicit
letterhead case.

Also syncs the n8n "Quote Outbound Send" workflow snapshot with two
more live fixes made during testing:
- Build attachments list: this.helpers.getBinaryDataBuffer() takes
  an item INDEX (number), not an item object -- per n8n's own docs.
  Was passing the item object, which threw an opaque non-Error value
  on every real send.
- Send Email (SMTP): the html field injected the plain-text
  email_message (with literal \n breaks) straight into HTML, so
  clients collapsed every paragraph into one block. Now converts \n
  to <br> first.
Also captures that the four WhatsApp-notify nodes and the workflow
itself are currently disabled/inactive in the live instance (manual
n8n edits during testing, not part of this fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 18:09:27 +02:00

732 lines
27 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"name": "WFDASH — Quote Outbound Send (push-payload + outbound-attachments)",
"description": "Sends approved quote emails with the auto-generated PDF AND any operator-selected outbound attachments (quotes.outbound_attachments). Triggered by Next.js flipping the quote to status='sending'. The Build attachments list Code node merges the PDF binary with each operator-selected outbound token (validated server-side in sendQuoteEmail and persisted as email_outbound_attachment_ids JSON), downloading the outbound files from PB. SMTP failure now flips the quote to mail_failed (was send_failed, which the retry path didn't recognise).",
"nodes": [
{
"parameters": {
"fromEmail": "=quotes@wfsales.co.za",
"toEmail": "={{ $('View customers').item.json.email }}",
"subject": "=QUOTE-{{ $('Search quotes').item.json.quote_number }} REF - {{ $('Search quotes').item.json.quote_reference }}",
"html": "={{ ($('Search quotes').item.json.email_message || '').replace(/\\n/g, '<br>') }}",
"options": {
"appendAttribution": false,
"attachments": "={{ Object.keys($binary || {}).join(',') }}",
"ccEmail": "={{ ($('Search quotes').item.json.email_cc || '').trim() }}",
"bccEmail": "=",
"replyTo": "quotes@wfsales.co.za"
}
},
"id": "8c00fc59-3492-42d3-8f3f-66a86eddfc31",
"name": "Send Email (SMTP)",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
-384,
448
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"webhookId": "d03c93e2-7bf8-4006-8542-5d94a87e68d1",
"onError": "continueErrorOutput"
},
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-2176,
352
],
"id": "81e80622-3c74-4aa5-b94e-638c0ba42507",
"name": "When clicking ‘Execute workflow’"
},
{
"parameters": {
"resource": "quotes",
"parameters": {
"elementsPerPage": 1,
"filter": "status = 'sending'"
},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
-1952,
448
],
"id": "9ffcb69c-e88c-477a-8b1b-d65f7b2db527",
"name": "Search quotes"
},
{
"parameters": {
"resource": "customers",
"operation": "view",
"elementId": "={{ $('Search quotes').item.json.customer }}",
"parameters": {},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
-1728,
448
],
"id": "eba6b537-d696-4d72-a794-85149c645fbd",
"name": "View customers"
},
{
"parameters": {
"url": "=https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io/api/files/{{ $json.collectionId }}/{{ $json.id }}/{{ $json.file[0] }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.5,
"position": [
-832,
448
],
"id": "d8fbb522-14b6-493a-ad78-9e0ae4b02eb2",
"name": "HTTP Request"
},
{
"parameters": {
"resource": "quote_attachments",
"parameters": {
"filter": "=quote = '{{ $('Search quotes').item.json.id }}' && kind = 'pdf'"
},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
-1280,
448
],
"id": "14fb9665-84b5-49f3-9f68-3d2d3e2a6167",
"name": "Search quote_attachments"
},
{
"parameters": {
"jsCode": "// Keep only the latest PDF attachment row per quote.\n// (Upstream `Search quote_attachments` already scopes to this quote's\n// id and kind = 'pdf'; no further filename filtering needed here.)\nconst items = $input.all();\n\nconst byQuote = {};\nfor (const item of items) {\n const quoteId = item.json.quote;\n const uploaded = new Date(item.json.uploaded_at).getTime();\n\n if (!byQuote[quoteId] || uploaded > byQuote[quoteId].uploaded) {\n byQuote[quoteId] = { item, uploaded };\n }\n}\n\nreturn Object.values(byQuote).map(entry => entry.item);"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1056,
448
],
"id": "17c9488c-6567-45fb-af45-2ecb841542a7",
"name": "Code in JavaScript"
},
{
"parameters": {
"resource": "quotes",
"operation": "update",
"elementId": "={{ $('Search quotes').item.json.id }}",
"parameters": {},
"fields": {
"assignments": [
{
"id": "200ad8f1-0104-439f-a8c4-56476dcf0e48",
"name": "status",
"value": "emailed",
"type": "string"
}
]
},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
-160,
352
],
"id": "22f9730d-a5e2-44aa-b93f-587b8e37848c",
"name": "Update quotes"
},
{
"parameters": {
"resource": "users",
"operation": "view",
"elementId": "={{ $('View customers').item.json.rep }}",
"parameters": {},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
288,
352
],
"id": "1d675ee9-9027-4fc9-ba82-a9b9ff83681c",
"name": "View users"
},
{
"parameters": {
"deviceIdOverride": "4ce3633b-f2e1-4732-9127-7efe46711b45",
"operation": "sendMedia",
"phoneNumber": "27828524633",
"mediaType": "file",
"caption": "={{ $('HTTP Request').item.json.filename }} \n{{ $('View customers').item.json.customer }}\n{{ $('View customers').item.json.email }}\n\nCLAIMED BY - {{ $('VIEW claimed').item.json.name }}\nQUOTE PROCESS TIME - {{ $('Code in JavaScript2').item.json.work_days_duration }}"
},
"type": "@aldinokemal2104/n8n-nodes-gowa.gowa",
"typeVersion": 1,
"position": [
1632,
256
],
"id": "0b6b950d-13b0-4d53-8acb-5d427afe40c6",
"name": "Send media files",
"disabled": true
},
{
"parameters": {
"url": "=https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io/api/files/{{ $json.collectionId }}/{{ $json.id }}/{{ $json.file[0] }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.5,
"position": [
1184,
352
],
"id": "230c9ea7-8fcd-4373-901b-b1def618ff17",
"name": "HTTP Request1"
},
{
"parameters": {
"resource": "quote_attachments",
"parameters": {
"filter": "=quote = '{{ $('Search quotes').item.json.id }}' && kind = 'pdf'"
},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
736,
352
],
"id": "79596ab4-97e5-46d9-960f-96e0392321a7",
"name": "Search quote_attachments1"
},
{
"parameters": {
"jsCode": "// Keep only the latest PDF attachment row per quote.\n// (Upstream `Search quote_attachments1` already scopes to this quote's\n// id and kind = 'pdf'; no further filename filtering needed here.)\nconst items = $input.all();\n\nconst byQuote = {};\nfor (const item of items) {\n const quoteId = item.json.quote;\n const uploaded = new Date(item.json.uploaded_at).getTime();\n\n if (!byQuote[quoteId] || uploaded > byQuote[quoteId].uploaded) {\n byQuote[quoteId] = { item, uploaded };\n }\n}\n\nreturn Object.values(byQuote).map(entry => entry.item);"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
352
],
"id": "01009e23-7df8-4b01-a203-0148373b36d1",
"name": "Code in JavaScript1"
},
{
"parameters": {
"resource": "users",
"operation": "view",
"elementId": "={{ $('Search quotes').item.json.claimed_by }}",
"parameters": {},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
512,
352
],
"id": "d312acc9-3b1b-4804-be96-47e386cf9feb",
"name": "VIEW claimed",
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"parameters": {
"resource": "users",
"operation": "view",
"elementId": "={{ $json.rep || 'eu0xus9z8le7n6v' }}",
"parameters": {},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
-1504,
448
],
"id": "217ab67d-8bbd-4fd8-bea7-1f2e936c0913",
"name": "View users1"
},
{
"parameters": {
"jsCode": "// Compute work-day duration between created and sending_started_at\nfunction workDaysBetween(startStr, endStr) {\n const start = new Date(startStr);\n const end = new Date(endStr);\n if (isNaN(start) || isNaN(end) || end < start) return 'N/A';\n\n const cur = new Date(start);\n cur.setHours(0, 0, 0, 0);\n const endDay = new Date(end);\n endDay.setHours(0, 0, 0, 0);\n\n let count = 0;\n while (cur < endDay) {\n cur.setDate(cur.getDate() + 1);\n const day = cur.getDay();\n if (day !== 0 && day !== 6) count++; // skip Sat/Sun\n }\n return count;\n}\n\nreturn $input.all().map(item => {\n const created = $('Search quotes').item.json.created;\n const processed = $('Search quotes').item.json.sending_started_at;\n const days = workDaysBetween(created, processed);\n return {\n json: {\n ...item.json,\n work_days_duration: days === 'N/A' ? 'N/A' : `${days} work day${days === 1 ? '' : 's'}`\n }\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
64,
352
],
"id": "49aa5965-281f-467c-9616-413d4de54fb7",
"name": "Code in JavaScript2"
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 4
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-2176,
544
],
"id": "e33773d4-da92-49e7-8e1a-a68854cd3150",
"name": "Schedule Trigger"
},
{
"parameters": {
"resource": "quotes",
"operation": "update",
"elementId": "={{ $('Search quotes').item.json.id }}",
"parameters": {},
"fields": {
"assignments": [
{
"id": "status-send-failed",
"name": "status",
"type": "string",
"value": "mail_failed"
}
]
},
"requestOptions": {}
},
"id": "3e2816ef-26fe-4e77-9033-725c1989c78c",
"name": "Update quote - Send Failed",
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
-160,
544
]
},
{
"parameters": {
"resource": "quote_attachments",
"parameters": {
"filter": "=quote = '{{ $('Search quotes').item.json.id }}' && kind = 'pdf'"
},
"requestOptions": {}
},
"type": "n8n-nodes-pocketbase.pocketbaseHttp",
"typeVersion": 1,
"position": [
64,
544
],
"id": "221aefad-c648-4e1f-9511-487b4a17f468",
"name": "SEARCH FAILED QUOTES"
},
{
"parameters": {
"jsCode": "// Keep only the latest PDF attachment row per quote.\n// (Upstream `SEARCH FAILED QUOTES` already scopes to this quote's\n// id and kind = 'pdf'; no further filename filtering needed here.)\nconst items = $input.all();\n\nconst byQuote = {};\nfor (const item of items) {\n const quoteId = item.json.quote;\n const uploaded = new Date(item.json.uploaded_at).getTime();\n\n if (!byQuote[quoteId] || uploaded > byQuote[quoteId].uploaded) {\n byQuote[quoteId] = { item, uploaded };\n }\n}\n\nreturn Object.values(byQuote).map(entry => entry.item);"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
288,
544
],
"id": "cacf85ba-3b38-4ddd-9a6b-aa9b6e7d290d",
"name": "FILTER FAILED QUOTES"
},
{
"parameters": {
"url": "=https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io/api/files/{{ $json.collectionId }}/{{ $json.id }}/{{ $json.file[0] }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.5,
"position": [
512,
544
],
"id": "7243dd8c-33b7-4b44-8f39-35d3ee5617e6",
"name": "DOWNLOAD FAILED QUOTE"
},
{
"parameters": {
"deviceIdOverride": "4ce3633b-f2e1-4732-9127-7efe46711b45",
"operation": "sendMedia",
"phoneNumber": "={{ $('View users1').item.json.tel }}",
"mediaType": "file",
"caption": "=QUOTE FAILDED TO SEND VIA EMAIL\n{{ $('HTTP Request').item.json.filename }} \n{{ $('View customers').item.json.customer }}\n{{ $('View customers').item.json.email }}\n"
},
"type": "@aldinokemal2104/n8n-nodes-gowa.gowa",
"typeVersion": 1,
"position": [
736,
544
],
"id": "2639682b-2714-400f-8a57-fe4d7f7823be",
"name": "SEND FAILED MESSAGE",
"disabled": true,
"onError": "continueErrorOutput"
},
{
"parameters": {
"deviceIdOverride": "4ce3633b-f2e1-4732-9127-7efe46711b45",
"operation": "sendMedia",
"phoneNumber": "={{ $('View users1').item.json.tel }}",
"mediaType": "file",
"caption": "={{ $('HTTP Request').item.json.filename }} \n{{ $('View customers').item.json.customer }}\n{{ $('View customers').item.json.email }}\n\nCLAIMED BY - {{ $('VIEW claimed').item.json.name }}\nQUOTE PROCESS TIME - {{ $('Code in JavaScript2').item.json.work_days_duration }}"
},
"type": "@aldinokemal2104/n8n-nodes-gowa.gowa",
"typeVersion": 1,
"position": [
1632,
448
],
"id": "9d9f79b3-9598-427d-8602-958e769e8529",
"name": "Send media files1",
"disabled": true
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "a2abe785-2f93-4e24-b0e2-baf9909ea940",
"leftValue": "={{ $('View users1').item.json.name }}",
"rightValue": "Gary",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1408,
352
],
"id": "db6ed47b-8a63-4c65-9260-87ab30683752",
"name": "If",
"disabled": true
},
{
"parameters": {
"jsCode": "// Build the outbound attachments payload for the SMTP send.\n//\n// n8n's SMTP node v2.1 reads attachments from BINARY properties on\n// the input item. Each file we want attached becomes a named\n// `binary.<name>` entry on the single item we return. The SMTP node's\n// `options.attachments` expression resolves\n// `Object.keys($binary).join(',')` and Nodemailer does the rest.\n//\n// Binary objects are built by hand ({data: base64, mimeType, fileName,\n// fileExtension}) instead of via `this.helpers.prepareBinaryData` --\n// that helper threw an opaque error on this instance (execution\n// 163729). A plain base64 `data` field is the same wire shape n8n\n// expects and doesn't need prepareBinaryData's extra bookkeeping.\n//\n// For the auto-generated PDF: `this.helpers.getBinaryDataBuffer()`\n// takes an ITEM INDEX (number), not an item object -- per n8n's docs,\n// `getBinaryDataBuffer(itemIndex, binaryPropertyName)`. Execution\n// 163852 was passing the upstream item object instead of an index,\n// which threw a non-Error opaque value. This node's only upstream\n// node is `HTTP Request` (single item in, single item out), so index\n// 0 is always the right item.\n//\n// For the operator-chosen outbound attachments, we download each\n// fresh from PB via `this.helpers.httpRequest` with `encoding:\n// 'arraybuffer'` (lowercase, matching n8n's IHttpRequestOptions --\n// `fetch` is NOT available in the Code node sandbox, and `encoding:\n// null` was removed in n8n 1.0+).\n\nconst PB_BASE = 'https://pocketbase-gnqc88suy8fqk6wyryjjdpvl.169.239.180.130.sslip.io';\n\nconst quoteRow = ($('Search quotes').first() || {}).json || {};\nconst quoteId = quoteRow.id;\nconst quoteNumber = quoteRow.quote_number || 'quote';\n\n// PDF metadata from the upstream `HTTP Request` node -- its binary.data\n// already contains the downloaded PDF bytes. We just rename it.\nconst pdfMeta = ($('HTTP Request').first() || {}).json || {};\nconst pdfFile = (pdfMeta.file && pdfMeta.file[0]) || '';\nconst pdfRawName = pdfMeta.filename || (quoteNumber + '.pdf');\nconst pdfName = String(pdfRawName)\n .replace(/[\\\\/:*?\"<>|]+/g, '_')\n .trim() || 'quote.pdf';\n\n// Outbound token intersection -- defends against drift between the\n// operator's modal selection and the row's denormalised\n// `outbound_attachments` field.\nconst allOutbound = Array.isArray(quoteRow.outbound_attachments)\n ? quoteRow.outbound_attachments\n : quoteRow.outbound_attachments\n ? [quoteRow.outbound_attachments]\n : [];\n\nlet selected = [];\ntry {\n const parsed = JSON.parse(quoteRow.email_outbound_attachment_ids || '[]');\n if (Array.isArray(parsed)) selected = parsed;\n} catch (e) {\n selected = [];\n}\n\nconst tokensToAttach = allOutbound.filter((t) => selected.includes(t));\n\nconst MIME_BY_EXT = {\n pdf: 'application/pdf',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n txt: 'text/plain',\n csv: 'text/csv',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n zip: 'application/zip',\n};\n\nconst binary = {};\nconst filenames = [];\nconst errors = [];\n\nfunction describeErr(err) {\n if (!err) return 'unknown error';\n if (err instanceof Error) {\n const own = Object.getOwnPropertyNames(err).filter((k) => k !== 'stack' && k !== 'message');\n const extra = own.length\n ? ' [' + own.map((k) => { try { return k + '=' + JSON.stringify(err[k]).slice(0, 200); } catch (e) { return k + '=<unserializable>'; } }).join(', ') + ']'\n : '';\n return (err.message || err.name || 'Error') + extra;\n }\n if (typeof err === 'string') return err;\n try {\n const own = Object.getOwnPropertyNames(err);\n if (own.length === 0) {\n return 'non-Error thrown value with no own properties: ' + Object.prototype.toString.call(err);\n }\n return JSON.stringify(err, own);\n } catch (e) {\n return String(err);\n }\n}\n\n// Build an n8n IBinaryData object directly from a Buffer/ArrayBuffer,\n// bypassing `this.helpers.prepareBinaryData` (see comment above).\nfunction bufferToBinaryData(buf, fileName, mime) {\n const ext = (fileName.split('.').pop() || '').toLowerCase();\n return {\n data: Buffer.from(buf).toString('base64'),\n mimeType: mime,\n fileName,\n fileExtension: ext,\n };\n}\n\nasync function downloadArrayBuffer(url) {\n const resp = await this.helpers.httpRequest({\n method: 'GET',\n url,\n encoding: 'arraybuffer',\n timeout: 15000,\n returnFullResponse: true,\n });\n const buf = resp && resp.body;\n const status = resp && resp.statusCode;\n if (!buf) {\n throw new Error('Empty response body (status ' + status + ')');\n }\n return buf;\n}\n\n// 1. Auto-generated PDF: rename the upstream binary.data -> binary.pdf.\nif (pdfFile && pdfMeta.id) {\n const upstream = ($('HTTP Request').first() || {}).binary || {};\n const pdfBin = upstream.data;\n if (pdfBin) {\n try {\n const buf = await this.helpers.getBinaryDataBuffer(0, 'data');\n binary.pdf = bufferToBinaryData(buf, pdfName, 'application/pdf');\n filenames.push(pdfName);\n } catch (err) {\n errors.push('PDF (' + pdfFile + '): ' + describeErr(err));\n }\n } else {\n errors.push('PDF (' + pdfFile + '): upstream HTTP Request produced no binary.data');\n }\n}\n\n// 2. Outbound files the operator chose.\nfor (let i = 0; i < tokensToAttach.length; i++) {\n const token = tokensToAttach[i];\n const idx = token.indexOf('_');\n const displayName = ((idx >= 0 ? token.slice(idx + 1) : token)\n .replace(/[\\\\/:*?\"<>|]+/g, '_')\n .trim()) || token;\n try {\n const url = PB_BASE + '/api/files/quotes/' + quoteId + '/' + token;\n const ext = displayName.split('.').pop().toLowerCase();\n const mime = MIME_BY_EXT[ext] || 'application/octet-stream';\n const buf = await downloadArrayBuffer.call(this, url);\n binary['outbound_' + i] = bufferToBinaryData(buf, displayName, mime);\n filenames.push(displayName);\n } catch (err) {\n errors.push(displayName + ': ' + describeErr(err));\n }\n}\n\n// Preserve the upstream item's json metadata so the SMTP node's\n// subject / html / to / cc expressions still resolve. We expose the\n// attachment metadata as JSON for logging on the dashboard.\nconst firstInput = $input.first() || {};\nconst json = Object.assign({}, firstInput.json || {}, pdfMeta, {\n attachment_count: filenames.length,\n attachment_filenames: filenames,\n attachment_errors: errors,\n});\n\nif (Object.keys(binary).length === 0) {\n return [{ json, binary: undefined }];\n}\n\nreturn [{ json, binary }];\n"
},
"id": "ab09c2d4-3f11-4d6e-a9a1-9b0d3c1f47e2",
"name": "Build attachments list",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-608,
448
]
}
],
"connections": {
"Send Email (SMTP)": {
"main": [
[
{
"node": "Update quotes",
"type": "main",
"index": 0
}
],
[
{
"node": "Update quote - Send Failed",
"type": "main",
"index": 0
}
]
]
},
"When clicking ‘Execute workflow’": {
"main": [
[
{
"node": "Search quotes",
"type": "main",
"index": 0
}
]
]
},
"Search quotes": {
"main": [
[
{
"node": "View customers",
"type": "main",
"index": 0
}
]
]
},
"View customers": {
"main": [
[
{
"node": "View users1",
"type": "main",
"index": 0
}
]
]
},
"Search quote_attachments": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"Update quotes": {
"main": [
[
{
"node": "Code in JavaScript2",
"type": "main",
"index": 0
}
]
]
},
"View users": {
"main": [
[
{
"node": "VIEW claimed",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request1": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Search quote_attachments1": {
"main": [
[
{
"node": "Code in JavaScript1",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript1": {
"main": [
[
{
"node": "HTTP Request1",
"type": "main",
"index": 0
}
]
]
},
"VIEW claimed": {
"main": [
[
{
"node": "Search quote_attachments1",
"type": "main",
"index": 0
}
]
]
},
"View users1": {
"main": [
[
{
"node": "Search quote_attachments",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript2": {
"main": [
[
{
"node": "View users",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Search quotes",
"type": "main",
"index": 0
}
]
]
},
"Update quote - Send Failed": {
"main": [
[
{
"node": "SEARCH FAILED QUOTES",
"type": "main",
"index": 0
}
]
]
},
"SEARCH FAILED QUOTES": {
"main": [
[
{
"node": "FILTER FAILED QUOTES",
"type": "main",
"index": 0
}
]
]
},
"FILTER FAILED QUOTES": {
"main": [
[
{
"node": "DOWNLOAD FAILED QUOTE",
"type": "main",
"index": 0
}
]
]
},
"DOWNLOAD FAILED QUOTE": {
"main": [
[
{
"node": "SEND FAILED MESSAGE",
"type": "main",
"index": 0
}
]
]
},
"If": {
"main": [
[
{
"node": "Send media files",
"type": "main",
"index": 0
}
],
[
{
"node": "Send media files1",
"type": "main",
"index": 0
},
{
"node": "Send media files",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Build attachments list",
"type": "main",
"index": 0
}
]
]
},
"Build attachments list": {
"main": [
[
{
"node": "Send Email (SMTP)",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": true
},
"staticData": {
"node:Schedule Trigger": {
"recurrenceRules": [
null
],
"recurrenceRuleSignatures": [
null
]
}
}
}