{ "name": "WFDASH — Purchase Order Send", "description": "Sends supplier PO emails with the latest PO PDF attached. Fired by Next.js route /api/v1/purchase-orders/[id]/send-email after it builds the full render-ready payload (recipient + subject + body + PDF attachment URL + AGENT_API_KEY + dashboard base URL). n8n never reads PocketBase or env vars directly. Records success/failure back to the dashboard via the outcome callback route.", "tags": [ { "name": "wfdash", "id": "wf-tag-wfdash" } ], "nodes": [ { "parameters": { "httpMethod": "POST", "path": "wf-po-send", "responseMode": "lastNode", "options": {} }, "id": "wf-po-send-webhook", "name": "PO Send Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [0, 0], "webhookId": "wf-po-send" }, { "parameters": { "jsCode": "// Resolve any attachment URLs into { name, content (base64), type }\n// entries the n8n emailSend node accepts. Returns the original input\n// shape unchanged when there are no attachments.\n//\n// n8n Code node: no $env.* access in this environment\n// (N8N_BLOCK_ENV_ACCESS_IN_NODE=true in docker-compose). The dashboard\n// route pushes everything we need (incl. the agentApiKey Bearer\n// credential) on the webhook body. The file proxy whitelist now\n// includes purchase_orders so the GET succeeds without further auth\n// headers from the cookie-less n8n-worker.\nconst first = ($input.first() || {}).json || {};\nconst body = first.body || first;\nconst attachments = body.attachments || [];\nconst agentApiKey = body.agentApiKey || '';\n\nif (!Array.isArray(attachments) || attachments.length === 0) {\n return [{ json: { ...first, body: { ...body, attachments: [] }, attachmentsResolved: [] } }];\n}\n\nconst resolved = [];\nfor (const att of attachments) {\n try {\n const res = await this.helpers.httpRequest({\n method: 'GET',\n url: att.url,\n headers: agentApiKey\n ? { 'Authorization': 'Bearer ' + agentApiKey }\n : {},\n encoding: 'base64',\n returnBinary: false,\n timeout: 15000,\n });\n resolved.push({\n name: att.filename || 'attachment.pdf',\n content: typeof res === 'string' ? res : (res && res.data) || '',\n type: att.contentType || 'application/pdf',\n });\n } catch (err) {\n return [{ json: {\n ...first,\n body: {\n ...body,\n attachments: [],\n _attachmentFetchError: 'PDF fetch failed: ' + (err.message || String(err)),\n },\n attachmentsResolved: [],\n } }];\n }\n}\n\nreturn [{ json: {\n ...first,\n body: { ...body, attachments: [] },\n attachmentsResolved: resolved,\n} }];" }, "id": "wf-po-send-fetch-pdf", "name": "Fetch PDF attachment bytes", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [220, 0] }, { "parameters": { "fromEmail": "={{ $json.body.fromEmail || 'orders@wfdash.local' }}", "toEmail": "={{ $json.body.recipient }}", "ccEmail": "={{ $json.body.cc }}", "bccEmail": "={{ $json.body.bcc }}", "subject": "={{ $json.body.subject }}", "text": "={{ $json.body.body }}", "html": "={{ $json.body.body }}", "options": { "attachments": "={{ $json.attachmentsResolved && $json.attachmentsResolved.length ? $json.attachmentsResolved : [] }}" } }, "id": "wf-po-send-smtp", "name": "Send PO email (SMTP)", "type": "n8n-nodes-base.emailSend", "typeVersion": 2.1, "position": [440, 0] }, { "parameters": { "jsCode": "// Resolve the SMTP result from the Send PO email (SMTP) node, then\n// POST the outcome back to the dashboard's\n// /api/v1/purchase-orders/[id]/send-email/outcome route. The dashboard\n// owns the canonical write (status flip + order_items.item_status flip)\n// using its own admin PB auth. n8n never carries the admin token.\n//\n// The Bearer key and dashboard base URL are pushed on the webhook body\n// (not $env.*) because the docker-compose has\n// N8N_BLOCK_ENV_ACCESS_IN_NODE=true — Code nodes cannot read env vars\n// at runtime.\nconst first = ($input.first() || {}).json || {};\nconst body = first.body || first;\nconst agentApiKey = body.agentApiKey || '';\nconst dashboardBase = (body.dashboardBaseUrl || '').replace(/\\/$/, '');\nconst poId = body.poId;\n\nif (!agentApiKey) {\n return [{ json: {\n poId,\n ok: false,\n error: 'agentApiKey missing from webhook body — route did not push it',\n } }];\n}\nif (!dashboardBase) {\n return [{ json: {\n poId,\n ok: false,\n error: 'dashboardBaseUrl missing from webhook body — route did not push it',\n } }];\n}\n\nconst sendResult = ($('Send PO email (SMTP)').first().json) || {};\nconst ok = !sendResult.error && sendResult.ok !== false;\nconst messageId = sendResult.messageId || sendResult.message_id || '';\nconst errorMessage = ok ? '' : (sendResult.error || sendResult.response || 'Unknown SMTP error').toString().slice(0, 990);\n\ntry {\n const out = await this.helpers.httpRequest({\n method: 'POST',\n url: dashboardBase + '/api/v1/purchase-orders/' + poId + '/send-email/outcome',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + agentApiKey,\n },\n json: true,\n body: {\n outcome: ok ? 'success' : 'failure',\n messageId,\n errorMessage,\n },\n });\n return [{ json: { poId, ok, messageId, error: errorMessage, outcome: out } }];\n} catch (err) {\n return [{ json: {\n poId,\n ok,\n messageId,\n error: 'Outcome callback failed: ' + (err.message || String(err)),\n } }];\n}" }, "id": "wf-po-send-record-outcome", "name": "Record outcome to PB", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [660, 0] } ], "connections": { "PO Send Webhook": { "main": [ [ { "node": "Fetch PDF attachment bytes", "type": "main", "index": 0 } ] ] }, "Fetch PDF attachment bytes": { "main": [ [ { "node": "Send PO email (SMTP)", "type": "main", "index": 0 } ] ] }, "Send PO email (SMTP)": { "main": [ [ { "node": "Record outcome to PB", "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-po-send", "templateCredsSetupCompleted": false, "description": "After import: open the workflow, attach an SMTP credential to the 'Send PO email (SMTP)' node (any name; the SDK does not pin a credential id). Then activate." } }