{ "name": "WFDASH — Quote Outbound Send", "description": "Sends approved quote emails via SMTP. Triggered when Next.js flips a quote to status='sending'. Records success (→ emailed) or failure (→ mail_failed) back to PocketBase with the SMTP response.", "tags": [ { "name": "wfdash", "id": "wf-tag-wfdash" } ], "nodes": [ { "parameters": { "events": [ "update" ], "collection": "quotes", "filter": "status='sending'", "options": { "expand": "customer" } }, "id": "trigger-pocketbase-quote-sending", "name": "PB: Quote status → sending", "type": "n8n-nodes-pocketbase.pocketbaseTrigger", "typeVersion": 1, "position": [ 0, 0 ], "webhookId": "wf-quote-outbound-send-trigger", "credentials": { "pocketbaseApi": { "id": "PB_CRED_ID", "name": "PocketBase account" } } }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "typeValidation": "loose", "version": 3 }, "conditions": [ { "leftValue": "={{ $json.record.status }}", "rightValue": "sending", "operator": { "type": "string", "operation": "equals" }, "id": "cond-status-sending" }, { "leftValue": "={{ $json.record.email_message_id }}", "rightValue": "", "operator": { "type": "string", "operation": "notEquals" }, "id": "cond-not-already-sent" } ], "combinator": "and" }, "options": {} }, "id": "if-status-sending", "name": "If status == sending", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 220, 0 ] }, { "parameters": { "method": "POST", "url": "={{ $env.WFDASH_API_BASE_URL }}/api/mcp", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" }, { "name": "X-Agent-Api-Key", "value": "={{ $env.AGENT_API_KEY }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"jsonrpc\": \"2.0\",\n \"method\": \"tools/call\",\n \"params\": {\n \"name\": \"quotes.get_send_payload\",\n \"arguments\": { \"quote_id\": \"{{ $json.record.id }}\" }\n },\n \"id\": \"1\"\n}", "options": { "response": { "response": { "responseFormat": "json" } }, "timeout": 15000 } }, "id": "mcp-fetch-send-payload", "name": "MCP: Get send payload", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 440, -120 ] }, { "parameters": { "jsCode": "// Compose the MIME message from the MCP payload. n8n's Send Email\n// node handles attachments + headers natively, so we only need to\n// shape the data it consumes.\nconst payload = $json.result?.data || $json.result || $json;\nconst quote = payload.quote || {};\nconst customer = quote.expand?.customer || quote.customer || {};\n\n// Build a deterministic RFC 5322 Message-ID per send. The format\n// embeds the quote id + epoch ms so retries get a different id\n// (RFC 5322 requires uniqueness).\nconst now = new Date();\nconst epoch = now.getTime();\nconst nonce = Math.random().toString(36).slice(2, 10);\nconst messageId = ``;\n\n// Pull the operator's persisted composition (saved at modal submit\n// time). Defaults come from MCP payload.\nconst subject = payload.subject || `Quote ${quote.quote_number}`;\nconst text = payload.message || `Please find attached quote ${quote.quote_number}.`;\n\n// Convert the JSON-encoded attachment IDs into a list of file URLs.\n// n8n's HTTP Request nodes download each file into $binary.\nconst attIds = Array.isArray(payload.selected_attachment_ids)\n ? payload.selected_attachment_ids\n : [];\nconst pdfId = payload.pdf_attachment_id;\nconst allIds = pdfId ? [pdfId, ...attIds.filter((x) => x !== pdfId)] : attIds;\n\nreturn {\n json: {\n quote_id: quote.id,\n quote_number: quote.quote_number,\n to: payload.to || customer.email,\n cc: payload.cc || \"\",\n bcc: payload.bcc || \"\",\n subject,\n text,\n from_name: payload.from_name || quote.company_letterhead || \"WFDASH\",\n attachment_ids: allIds,\n message_id: messageId,\n compose_ts: now.toISOString()\n }\n};\n" }, "id": "code-format-email", "name": "Format email params", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 660, -120 ] }, { "parameters": { "method": "GET", "url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/files/quote_attachments/{{ $json.attachment_id }}/{{ $json.filename }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $env.PB_ADMIN_TOKEN }}" } ] }, "options": { "response": { "response": { "responseFormat": "file" } }, "timeout": 30000 } }, "id": "http-fetch-attachment", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 880, -120 ] }, { "parameters": { "fromEmail": "=quotes@wfsales.co.za", "toEmail": "={{ $json.to }}", "subject": "={{ $json.subject }}", "emailFormat": "text", "text": "={{ $json.text }}", "options": { "appendAttribution": false, "ccEmail": "={{ $json.cc }}", "bccEmail": "={{ $json.bcc }}", "replyTo": "quotes@wfsales.co.za", "attachments": "={{ $binary }}" } }, "id": "send-email-smtp", "name": "Send Email (SMTP)", "type": "n8n-nodes-base.emailSend", "typeVersion": 2.1, "position": [ 1100, -120 ], "retryOnFail": true, "maxTries": 3, "waitBetweenTries": 5000, "onError": "continueRegularOutput", "credentials": { "smtp": { "id": "SMTP_CRED_ID", "name": "WFDASH Outbound SMTP" } } }, { "parameters": { "method": "POST", "url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/collections/pbc_2527524235/records/{{ $node[\"Format email params\"].json.quote_id }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" }, { "name": "Authorization", "value": "={{ $env.PB_ADMIN_TOKEN }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"status\": \"emailed\",\n \"email_delivered\": true,\n \"email_delivered_at\": \"{{ $now.toISO() }}\",\n \"email_outbound_message_id\": \"{{ $node[\"Format email params\"].json.message_id }}\",\n \"email_error\": \"\",\n \"sending_started_at\": \"\"\n}", "options": { "timeout": 15000 } }, "id": "patch-quote-emailed", "name": "PATCH: quote → emailed", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1320, -200 ] }, { "parameters": { "method": "POST", "url": "={{ $env.NEXT_PUBLIC_POCKETBASE_URL }}/api/collections/pbc_2527524235/records/{{ $node[\"Format email params\"].json.quote_id }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Content-Type", "value": "application/json" }, { "name": "Authorization", "value": "={{ $env.PB_ADMIN_TOKEN }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"status\": \"mail_failed\",\n \"email_failed_at\": \"{{ $now.toISO() }}\",\n \"email_error\": \"[smtp] {{ ($json.error?.message || $json.message || 'Unknown SMTP failure').toString().slice(0, 4900) }}\",\n \"email_outbound_message_id\": \"{{ $node[\"Format email params\"].json.message_id }}\",\n \"sending_started_at\": \"\"\n}", "options": { "timeout": 15000 } }, "id": "patch-quote-mail-failed", "name": "PATCH: quote → mail_failed", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1320, -40 ] } ], "connections": { "PB: Quote status → sending": { "main": [ [ { "node": "If status == sending", "type": "main", "index": 0 } ] ] }, "If status == sending": { "main": [ [ { "node": "MCP: Get send payload", "type": "main", "index": 0 } ] ] }, "MCP: Get send payload": { "main": [ [ { "node": "Format email params", "type": "main", "index": 0 } ] ] }, "Format email params": { "main": [ [ { "node": "Send Email (SMTP)", "type": "main", "index": 0 } ] ] }, "Send Email (SMTP)": { "main": [ [ { "node": "PATCH: quote → emailed", "type": "main", "index": 0 } ] ], "error": [ [ { "node": "PATCH: quote → mail_failed", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "saveDataSuccessExecution": "all", "saveDataErrorExecution": "all", "saveManualExecutions": true, "saveExecutionProgress": true, "timezone": "Africa/Johannesburg" }, "staticData": null, "pinData": {}, "versionId": "1", "triggerCount": 1, "meta": { "instanceId": "wfdash-quote-outbound-send", "templateCredsSetupCompleted": false, "description": "SMTP creds + PocketBase creds + WFDASH API base URL + AGENT_API_KEY env vars must be configured in n8n before activation. Add a new SMTP credential named 'WFDASH Outbound SMTP' and a PocketBase credential named 'PocketBase account' before publishing." } }