{
  "id": "qqEyPIiXqpshnSKv",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Create a work4all incoming invoice from email attachments using AI",
  "tags": [],
  "nodes": [
    {
      "id": "e66e1fd3-d063-4ee3-9c6f-62cf8b3e0776",
      "name": "Sticky Note - Description",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        12368,
        7072
      ],
      "parameters": {
        "width": 460,
        "height": 680,
        "content": "## Create a work4all incoming invoice from email attachments using AI\n\nThis **sub-workflow** is called by a parent workflow with one email item whose attachments are passed as binary data. The attachments are read by AI, structured invoice data is extracted, and the **work4all** node books the incoming invoice with the original files attached.\n\n### How it works\n1. Keeps only emails that carry a readable PDF / XML / JSON attachment.\n2. Extracts invoice data in one of two modes (**Extraction config**):\n   - **`backend`** (default) \u2014 work4all AI backend, uses the **work4all** credential.\n   - **`llm`** \u2014 your own **OpenAI-compatible** model (*Call LLM*).\n3. Normalises the result, books the incoming invoice and attaches the files.\n\n### Setup\n1. Add your **work4all** credential.\n2. Call this workflow from a parent that supplies the email + attachments.\n\n### Customization\n- **Extraction mode** \u2014 default is `backend` (work4all AI). For your own model, set **Extraction config** to `llm` and add an **OpenAI-compatible** credential on *Call LLM*.\n- Switch provider/model in *Call LLM* and the `MODEL` constant in *Build LLM messages* (must support **PDF / file input**).\n\nRequires a work4all account and the verified `n8n-nodes-work4all` node."
      },
      "typeVersion": 1
    },
    {
      "id": "78f97643-5f70-480a-b97b-026145b05ebe",
      "name": "Sticky Note - Input",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        12912,
        7680
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 368,
        "content": "## 1 \u00b7 Input & validation\nReceives the email from the parent workflow and keeps only items that carry a readable PDF / XML / JSON attachment."
      },
      "typeVersion": 1
    },
    {
      "id": "637606f7-39aa-4f71-99eb-988f55e33661",
      "name": "Sticky Note - Extract",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        14304,
        7376
      ],
      "parameters": {
        "color": 7,
        "width": 470,
        "height": 492,
        "content": "## 3 \u00b7 Extract invoice data\nBoth modes read the attachments and return the same structured invoice JSON \u2014 via the work4all AI backend or an OpenAI-compatible LLM (*Build LLM messages* \u2192 *Call LLM*)."
      },
      "typeVersion": 1
    },
    {
      "id": "455c54ab-3f25-4139-a6d0-ae08ce274bf6",
      "name": "Sticky Note - Book",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        15520,
        7488
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 312,
        "content": "## 5 \u00b7 Book in work4all\nNormalises currency and dates, creates the incoming invoice in work4all and attaches the original files."
      },
      "typeVersion": 1
    },
    {
      "id": "8dac390a-f06a-4a85-bb4f-130559fc4448",
      "name": "When called by parent workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        12960,
        7872
      ],
      "parameters": {
        "inputSource": "passthrough"
      },
      "typeVersion": 1.1
    },
    {
      "id": "592be2b9-c567-4474-b715-ecbd62727f5a",
      "name": "Has attachments?",
      "type": "n8n-nodes-base.if",
      "position": [
        13184,
        7872
      ],
      "parameters": {
        "options": {
          "ignoreCase": false
        },
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "7ea39fb4-a88f-4f20-85d2-6daea27f1c16",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ Object.keys($binary ?? {}).length > 0 }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "166ec2d0-b6a8-464c-b876-1ad81472a68c",
      "name": "Build LLM messages",
      "type": "n8n-nodes-base.code",
      "position": [
        14352,
        7696
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// \u2699\ufe0f CONFIG: set your OpenAI-compatible model here\nconst MODEL = \"policy/EU-Sonnet-4-6\";\n\nconst item = $input.item;\n\nlet content = [\n  {\n    type: \"text\",\n    text: `\nReturn ONLY valid JSON.\nDo not use markdown.\nDo not wrap in triple backticks.\nDo not add explanations before or after the JSON.\n\nUse exactly this shape:\n{\n  \"found_invoice\": true,\n  \"reason\": \"short explanation\",\n  \"relevantAttachmentKeys\": [\"attachment_0\"],\n  \"invoiceData\": {\n    \"supplierCode\": null,\n    \"supplierName\": null,\n    \"supplierCustomerNumberAtSupplier\": null,\n    \"supplierContactMailAddress\": null,\n    \"supplierIban\": null,\n    \"projectCode\": null,\n    \"projectNumber\": null,\n    \"projectName\": null,\n    \"invoiceNumberSupplier\": null,\n    \"note\": null,\n    \"invoiceDate\": null,\n    \"paymentTermDays\": null,\n    \"discount1Rate\": null,\n    \"discount1Days\": null,\n    \"discount2Rate\": null,\n    \"discount2Days\": null,\n    \"currencyCode\": null,\n    \"invoiceItems\": [\n      {\n        \"account\": null,\n        \"costCenter\": null,\n        \"costGroup\": null,\n        \"projectCode\": null,\n        \"taxRate\": null,\n        \"netAmount\": null,\n        \"grossAmount\": null,\n        \"vatAmount\": null,\n        \"note\": null\n      }\n    ]\n  }\n}\n\nRules:\n- found_invoice = true only if at least one attachment is clearly an incoming supplier invoice / Eingangsrechnung / XRechnung / receipt relevant for accounting.\n- relevantAttachmentKeys must contain the original attachment keys like \"attachment_0\".\n- invoiceData field names must match exactly.\n- The note field should hold a short description, e.g. the supplier company name combined with the invoice period.\n- Each invoiceItems note should be a short note, not more than 50 characters.\n- Dates must be ISO-8601 strings if known, otherwise null.\n- Do not invent values.\n- Aggregate invoiceItems by VAT rate (Mehrwertsteuersatz), not by raw line item.\n- Return one invoiceItems entry per VAT bucket.\n- Sum netAmount, vatAmount and grossAmount per VAT bucket.\n- Only fill account, costCenter, costGroup, projectCode, taxCode if explicitly present in the documents.\n- If no invoice is found, return:\n  {\n    \"found_invoice\": false,\n    \"reason\": \"...\",\n    \"relevantAttachmentKeys\": [],\n    \"invoiceData\": {\n      \"supplierCode\": null,\n      \"supplierName\": null,\n      \"supplierCustomerNumberAtSupplier\": null,\n      \"supplierContactMailAddress\": null,\n      \"supplierIban\": null,\n      \"projectCode\": null,\n      \"projectNumber\": null,\n      \"projectName\": null,\n      \"invoiceNumberSupplier\": null,\n      \"note\": null,\n      \"invoiceDate\": null,\n      \"paymentTermDays\": null,\n      \"discount1Rate\": null,\n      \"discount1Days\": null,\n      \"discount2Rate\": null,\n      \"discount2Days\": null,\n      \"currencyCode\": null,\n      \"invoiceItems\": []\n    }\n  }\n`\n  }\n];\n\nlet has_xml_or_json = false;\n\nif (item.binary) {\n  for (const key of Object.keys(item.binary)) {\n    const file = item.binary[key];\n\n    const mimeType = file.mimeType || \"\";\n    const fileName = file.fileName || \"file\";\n\n    const buffer = await this.helpers.getBinaryDataBuffer(0, key);\n    const base64 = buffer.toString(\"base64\");\n\n    const isPdf =\n      mimeType.includes(\"pdf\") ||\n      fileName.toLowerCase().endsWith(\".pdf\");\n\n    if (isPdf) {\n      has_xml_or_json = true;\n      content.push({\n        type: \"input_file\",\n        filename: fileName,\n        file_data: `data:application/pdf;base64,${base64}`\n      });\n    } else if (\n      mimeType.includes(\"xml\") ||\n      mimeType.includes(\"json\") ||\n      fileName.endsWith(\".xml\") ||\n      fileName.endsWith(\".json\")\n    ) {\n      has_xml_or_json = true;\n      const text = buffer.toString(\"utf-8\");\n\n      content.push({\n        type: \"text\",\n        text: `Content of ${fileName}:\\n${text}`\n      });\n    }\n  }\n}\n\nreturn {\n  json: {\n    has_xml_or_json: has_xml_or_json,\n    model: MODEL,\n    messages: [\n      {\n        role: \"user\",\n        content\n      }\n    ]\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "f656a11f-2883-48ac-8b73-3a9151cd4237",
      "name": "Has readable attachment?",
      "type": "n8n-nodes-base.if",
      "position": [
        13424,
        7776
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a5d662cb-f633-4648-bc88-bcc9362b9630",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ Object.values($binary ?? {}).some(f => /pdf|xml|json/i.test(((f.mimeType || '') + ' ' + (f.fileName || '')))) }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "9fc590c7-a747-4619-9612-df85a34d6bb7",
      "name": "Call LLM",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        14576,
        7696
      ],
      "parameters": {
        "url": "https://router.eu.requesty.ai/v1/chat/completions",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "typeVersion": 4.4
    },
    {
      "id": "416e69af-4831-4438-bc55-47645db328b4",
      "name": "Parse extraction result",
      "type": "n8n-nodes-base.code",
      "position": [
        14976,
        7600
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "function removeNullish(value) {\n  if (Array.isArray(value)) {\n    return value.map(removeNullish);\n  }\n  if (value && typeof value === \"object\") {\n    return Object.fromEntries(\n      Object.entries(value)\n        .filter(([, v]) => v !== undefined && v !== null && v !== \"\")\n        .map(([k, v]) => [k, removeNullish(v)])\n    );\n  }\n  return value;\n}\n\n// LLM path returns an OpenAI envelope ($json.choices[0].message.content);\n// the work4all AI backend returns the payload directly (often as a plain JSON string).\nlet raw = $json && $json.choices ? $json.choices[0].message.content : $json;\n\nlet parsed;\nif (typeof raw === \"string\") {\n  raw = raw\n    .replace(/^```json\\s*/i, \"\")\n    .replace(/^```\\s*/i, \"\")\n    .replace(/\\s*```$/, \"\")\n    .trim();\n  parsed = raw ? JSON.parse(raw) : {};\n} else {\n  parsed = raw ?? {};\n}\n\nreturn {\n  json: {\n    found_invoice: parsed.found_invoice === true,\n    reason: parsed.reason ?? \"\",\n    relevantAttachmentKeys: parsed.relevantAttachmentKeys ?? [],\n    invoiceData: removeNullish(parsed.invoiceData ?? {}),\n  },\n  binary: $input.item.binary,\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "6ce97f81-cbb8-4c0d-af82-2f2ee828293c",
      "name": "Has invoice?",
      "type": "n8n-nodes-base.if",
      "position": [
        15200,
        7600
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "6573bc49-61be-476e-95d7-3f9b4e26b1c9",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.found_invoice }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "a76861d5-716f-48d8-bf94-6174f511f99d",
      "name": "Filter attachments & normalize",
      "type": "n8n-nodes-base.code",
      "position": [
        15568,
        7584
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const relevantKeys = $json.relevantAttachmentKeys || [];\nconst sourceBinary = $('When called by parent workflow').item.binary || {};\n\nconst keptBinary = Object.fromEntries(\n  relevantKeys\n    .filter((key) => sourceBinary[key])\n    .map((key) => [key, sourceBinary[key]])\n);\n\nconst invoiceData = { ...($json.invoiceData || {}) };\ninvoiceData.currency = \"EUR\";\ndelete invoiceData.currencyCode;\n\nfor (const key of [\"invoiceDate\", \"entryDate\", \"receiptDate\"]) {\n  if (\n    typeof invoiceData[key] === \"string\" &&\n    /^\\d{4}-\\d{2}-\\d{2}$/.test(invoiceData[key])\n  ) {\n    invoiceData[key] = `${invoiceData[key]}T00:00:00Z`;\n  }\n}\n\nconst receiptDate = $('When called by parent workflow').item.json.receivedDateTime;\n\ninvoiceData.receiptDate = receiptDate;\ninvoiceData.entryDate = receiptDate;\n\nreturn {\n  json: {\n    ...$json,\n    invoiceData,\n    relevantAttachmentKeys: relevantKeys,\n  },\n  binary: keptBinary,\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "14556c93-cc8c-41bb-9b00-e2cd136406c0",
      "name": "Create incoming invoice",
      "type": "n8n-nodes-work4all.work4all",
      "position": [
        15792,
        7584
      ],
      "parameters": {
        "dataMode": "json",
        "attachmentsJson": "={{ ($json.relevantAttachmentKeys || []).map(k => ({ binaryPropertyName: k })) }}",
        "attachmentsMode": "json",
        "invoiceDataJson": "={{$json.invoiceData}}"
      },
      "typeVersion": 1
    },
    {
      "id": "b6f276dc-7eb3-4022-a8a2-53267f220c80",
      "name": "Format work4all result",
      "type": "n8n-nodes-base.code",
      "position": [
        16016,
        7584
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return {\n  json: {\n    messageId: $('When called by parent workflow').item.json.id,\n    errors: $json.errors,\n    invoiceData: $json.data\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "30af0f13-15e8-482a-8209-a000b190baa3",
      "name": "No invoice - skip",
      "type": "n8n-nodes-base.noOp",
      "position": [
        15776,
        7968
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "75dcd732-3065-4e5a-bba9-f87fdda02d50",
      "name": "Build result (no invoice)",
      "type": "n8n-nodes-base.code",
      "position": [
        16016,
        7968
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "return {\n  json: {\n    messageId: $('When called by parent workflow').item.json.id,\n    success: true\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "cc7f976e-5085-42ab-9a8e-7a78155009d3",
      "name": "Extraction config",
      "type": "n8n-nodes-base.set",
      "position": [
        13744,
        7600
      ],
      "parameters": {
        "options": {
          "stripBinary": false
        },
        "assignments": {
          "assignments": [
            {
              "id": "a1b2c3d4-+123456789001",
              "name": "extractionMode",
              "type": "string",
              "value": "backend"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "cc5a7d76-6063-4f73-a15e-3c4382b0bb8e",
      "name": "Use work4all AI backend?",
      "type": "n8n-nodes-base.if",
      "position": [
        13968,
        7600
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "b1b2c3d4-+123456789002",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.extractionMode }}",
              "rightValue": "backend"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "2c7e4d91-6af3-458a-9fa7-f4d32917b300",
      "name": "Extract invoice data",
      "type": "n8n-nodes-work4all.work4all",
      "position": [
        14576,
        7504
      ],
      "parameters": {
        "operation": "extractInvoiceData"
      },
      "credentials": {
        "work4allOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "147f7d49-6d33-4547-a819-86c90e39ca4a",
      "name": "Sticky Note - Extraction mode",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        13712,
        7472
      ],
      "parameters": {
        "color": 7,
        "width": 446,
        "height": 336,
        "content": "## 2 \u00b7 Choose extraction mode\nSet `extractionMode` in **Extraction config**: `backend` (work4all AI backend) or `llm` (your own OpenAI-compatible model)."
      },
      "typeVersion": 1
    },
    {
      "id": "4e9262e1-c41b-481c-97d4-8678e1cd5810",
      "name": "Sticky Note - Parse",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        14928,
        7488
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 340,
        "content": "## 4 \u00b7 Parse & check\nNormalises either output into the same shape and checks whether an invoice was found. If not, booking is skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "68c12587-e1d6-4efa-8be4-58b5403e4bb1",
      "name": "Sticky Note - No invoice",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        15728,
        7872
      ],
      "parameters": {
        "color": 7,
        "width": 488,
        "height": 256,
        "content": "## 6 \u00b7 No invoice\nItems without a usable invoice end here and return a simple skipped result."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "987ac2a4-e699-42e6-ba8b-35ccc0c4fbe7",
  "connections": {
    "Call LLM": {
      "main": [
        [
          {
            "node": "Parse extraction result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has invoice?": {
      "main": [
        [
          {
            "node": "Filter attachments & normalize",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No invoice - skip",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has attachments?": {
      "main": [
        [
          {
            "node": "Has readable attachment?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No invoice - skip",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extraction config": {
      "main": [
        [
          {
            "node": "Use work4all AI backend?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No invoice - skip": {
      "main": [
        [
          {
            "node": "Build result (no invoice)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build LLM messages": {
      "main": [
        [
          {
            "node": "Call LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract invoice data": {
      "main": [
        [
          {
            "node": "Parse extraction result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create incoming invoice": {
      "main": [
        [
          {
            "node": "Format work4all result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse extraction result": {
      "main": [
        [
          {
            "node": "Has invoice?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has readable attachment?": {
      "main": [
        [
          {
            "node": "Extraction config",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No invoice - skip",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use work4all AI backend?": {
      "main": [
        [
          {
            "node": "Extract invoice data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build LLM messages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter attachments & normalize": {
      "main": [
        [
          {
            "node": "Create incoming invoice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When called by parent workflow": {
      "main": [
        [
          {
            "node": "Has attachments?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}