The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "AutomationNoteSwissQuote01",
"name": "AUTOMATION NOTE Swiss Exact-Match Quote Prototype",
"description": null,
"active": false,
"isArchived": false,
"nodes": [
{
"parameters": {
"content": "## Validate extracted quote items against a trusted catalog\n\n### Who this is for\nOperations and automation teams that use AI or OCR to extract parts, labor, and travel data before preparing a quote. The workflow prevents extracted references or claimed prices from becoming trusted totals without checks.\n\n### What it does\nA POST webhook receives a synthetic request. The first Code node validates request IDs, text length, labor hours, booleans, item count, reference format, and quantity limits. Valid candidates move to a fixed synthetic catalog. Claimed unit prices are ignored; only exact catalog references supply prices. Calculations use integer cents for materials, margin, labor, travel, VAT, and total.\n\nThe workflow returns three explicit outcomes:\n- `200 QUOTABLE` when every item matches.\n- `422 NEEDS_REVIEW` with no totals when any reference is missing.\n- `400 REJECTED` when input validation fails.\n\n### How to set up\n1. Import the workflow and keep it inactive.\n2. Add webhook authentication before accepting external traffic.\n3. Replace the synthetic catalog and rates with an approved data source and documented rules.\n4. Test accepted, missing-reference, malicious-price, and invalid-quantity cases.\n5. Add human approval before any quote is sent.\n\n### Requirements and customization\nUses only built-in Webhook, Code, If, and Respond to Webhook nodes. No credentials, external APIs, customer data, or LLM calls are included. Customize the input contract, catalog lookup, currency, tax, rounding, and response codes. Reproducible test evidence: [setup and cases](https://automation-note.com/articles/n8n-swiss-quote-prototype/).",
"height": 980,
"width": 760,
"color": 4
},
"id": "b77adf35-4ec5-417f-8de9-e8e5340d6256",
"name": "Template Guide",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-880,
-340
]
},
{
"parameters": {
"content": "### 1. Validate the untrusted input\nThe webhook receives synthetic extraction data. Replace `authentication: none` before production.",
"height": 520,
"width": 700,
"color": 7
},
"id": "86f56655-2f88-4677-ad06-425a841dff0b",
"name": "Input Validation Guide",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-80,
-80
]
},
{
"parameters": {
"content": "### 2. Match only trusted catalog records\nIgnore claimed prices. Unknown references stop with `NEEDS_REVIEW` and no total.",
"height": 520,
"width": 500,
"color": 7
},
"id": "0ccbcc86-19b4-43fc-ae8e-0d2d2be64a51",
"name": "Catalog Match Guide",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
700,
-80
]
},
{
"parameters": {
"content": "### 3. Return explicit outcomes\nUse separate responses for accepted, review, and rejected requests.",
"height": 520,
"width": 360,
"color": 7
},
"id": "60d12bae-6f67-49ad-8f4d-b74f7392f7af",
"name": "Response Outcome Guide",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1220,
-80
]
},
{
"parameters": {
"jsCode": "return $input.all().map((item) => ({ json: {\n ok: false,\n status: 'REJECTED',\n request_id: item.json.request_id,\n invalid_fields: item.json.invalid_fields\n} }));"
},
"id": "8bb4a0de-555c-4e0a-ba4a-4ed74c4e82b5",
"name": "Build Rejected Response",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
780,
280
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "swiss-quote-prototype",
"authentication": "none",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "https://automation-note.com"
}
},
"id": "8393076b-bbcf-4aad-88c3-1be227eb03a1",
"name": "Receive Synthetic Request",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
0,
160
]
},
{
"parameters": {
"jsCode": "const input = $json.body ?? $json;\nconst requestId = typeof input.request_id === 'string' ? input.request_id : '';\nconst sourceDescription = typeof input.source_description === 'string' ? input.source_description : '';\nconst laborHours = input.labor_hours;\nconst travelRequired = input.travel_required;\nconst extractedItems = Array.isArray(input.extracted_items) ? input.extracted_items : [];\nconst errors = [];\nif (!/^quote-[a-z0-9-]{3,40}$/i.test(requestId)) errors.push('request_id');\nif (!sourceDescription || sourceDescription.length > 1000) errors.push('source_description');\nconst laborHundredths = typeof laborHours === 'number' ? laborHours * 100 : Number.NaN;\nif (typeof laborHours !== 'number' || !Number.isFinite(laborHours) || laborHours < 0 || laborHours > 24 || Math.abs(Math.round(laborHundredths) - laborHundredths) > 1e-9) errors.push('labor_hours');\nif (typeof travelRequired !== 'boolean') errors.push('travel_required');\nif (extractedItems.length < 1 || extractedItems.length > 10) errors.push('extracted_items');\nconst normalizedItems = extractedItems.map((item, index) => {\n const reference = typeof item?.reference === 'string' ? item.reference : '';\n const quantity = item?.quantity;\n if (!/^SYN-[A-Z]+-[0-9]{3}$/.test(reference)) errors.push(`extracted_items[${index}].reference`);\n if (!Number.isInteger(quantity) || quantity < 1 || quantity > 20) errors.push(`extracted_items[${index}].quantity`);\n if (item?.claimed_unit_price_chf !== undefined && (typeof item.claimed_unit_price_chf !== 'number' || !Number.isFinite(item.claimed_unit_price_chf))) errors.push(`extracted_items[${index}].claimed_unit_price_chf`);\n return { reference, quantity, claimed_unit_price_chf: item?.claimed_unit_price_chf ?? null };\n});\nreturn [{ json: {\n ok: errors.length === 0,\n status: errors.length === 0 ? 'VALIDATED' : 'REJECTED',\n request_id: requestId || null,\n invalid_fields: [...new Set(errors)],\n source_description: sourceDescription,\n labor_hours: laborHours,\n travel_required: travelRequired,\n extracted_items: normalizedItems,\n extraction_boundary: 'untrusted_synthetic_fixture'\n} }];"
},
"id": "44e9da82-fae1-4732-b21b-24217c6c46f3",
"name": "Validate Untrusted Extraction",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
260,
160
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.ok }}",
"operation": "equal",
"value2": true
}
]
},
"combineOperation": "all"
},
"id": "ab1a8413-08a8-4f0e-94d8-80018ed67d04",
"name": "Is Input Valid",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
520,
160
]
},
{
"parameters": {
"jsCode": "const catalog = Object.freeze({\n 'SYN-VALVE-100': Object.freeze({ name: 'Synthetic isolation valve 100', unit_price_cents: 12000 }),\n 'SYN-PIPE-200': Object.freeze({ name: 'Synthetic pipe section 200', unit_price_cents: 8550 }),\n 'SYN-SEAL-300': Object.freeze({ name: 'Synthetic seal kit 300', unit_price_cents: 3240 })\n});\nconst centsToChf = (value) => Number((value / 100).toFixed(2));\nconst matchedItems = [];\nconst missingReferences = [];\nlet ignoredClaimedPrices = false;\nfor (const item of $json.extracted_items) {\n const catalogItem = catalog[item.reference];\n if (!catalogItem) {\n missingReferences.push(item.reference);\n continue;\n }\n if (item.claimed_unit_price_chf !== null && Math.round(item.claimed_unit_price_chf * 100) !== catalogItem.unit_price_cents) ignoredClaimedPrices = true;\n const lineTotalCents = catalogItem.unit_price_cents * item.quantity;\n matchedItems.push({\n reference: item.reference,\n name: catalogItem.name,\n quantity: item.quantity,\n unit_price_chf: centsToChf(catalogItem.unit_price_cents),\n line_total_chf: centsToChf(lineTotalCents),\n line_total_cents: lineTotalCents\n });\n}\nif (missingReferences.length > 0) {\n return [{ json: {\n ok: false,\n status: 'NEEDS_REVIEW',\n request_id: $json.request_id,\n exact_match: false,\n matched_items: matchedItems.map(({ line_total_cents, ...item }) => item),\n missing_references: missingReferences,\n ignored_claimed_prices: ignoredClaimedPrices,\n totals: null,\n reason: 'exact_catalog_match_required'\n } }];\n}\nconst materialsCents = matchedItems.reduce((sum, item) => sum + item.line_total_cents, 0);\nconst marginCents = Math.round(materialsCents * 0.15);\nconst laborCents = Math.round($json.labor_hours * 9500);\nconst travelCents = $json.travel_required ? 4500 : 0;\nconst netCents = materialsCents + marginCents + laborCents + travelCents;\nconst vatCents = Math.round(netCents * 0.081);\nconst totalCents = netCents + vatCents;\nreturn [{ json: {\n ok: true,\n status: 'QUOTABLE',\n request_id: $json.request_id,\n exact_match: true,\n matched_items: matchedItems.map(({ line_total_cents, ...item }) => item),\n missing_references: [],\n ignored_claimed_prices: ignoredClaimedPrices,\n rates: { material_margin_percent: 15, labor_hour_chf: 95, travel_flat_chf: 45, vat_percent: 8.1 },\n totals: {\n materials_chf: centsToChf(materialsCents),\n material_margin_chf: centsToChf(marginCents),\n labor_chf: centsToChf(laborCents),\n travel_chf: centsToChf(travelCents),\n net_chf: centsToChf(netCents),\n vat_chf: centsToChf(vatCents),\n total_chf: centsToChf(totalCents)\n }\n} }];"
},
"id": "f8edc20b-47ee-4a7d-b6df-a8724442150d",
"name": "Exact Match and Calculate",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
780,
80
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.ok }}",
"operation": "equal",
"value2": true
}
]
},
"combineOperation": "all"
},
"id": "3425d12f-80c4-4c64-9dde-1e0bb89b8f0b",
"name": "Is Quote Ready",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1040,
80
]
},
{
"parameters": {
"respondWith": "firstIncomingItem",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "9f999379-8ddd-4675-a9ec-d09e033dc87c",
"name": "Respond Quotable",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1300,
0
]
},
{
"parameters": {
"respondWith": "firstIncomingItem",
"options": {
"responseCode": 422,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "be0f7ac1-6490-4848-9fc9-9b4dfaf98a4f",
"name": "Respond Needs Review",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1300,
160
]
},
{
"parameters": {
"respondWith": "firstIncomingItem",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "93bdcb43-aacb-4609-9045-ebc87ce031a3",
"name": "Respond Rejected",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1040,
280
]
}
],
"connections": {
"Receive Synthetic Request": {
"main": [
[
{
"node": "Validate Untrusted Extraction",
"type": "main",
"index": 0
}
]
]
},
"Validate Untrusted Extraction": {
"main": [
[
{
"node": "Is Input Valid",
"type": "main",
"index": 0
}
]
]
},
"Is Input Valid": {
"main": [
[
{
"node": "Exact Match and Calculate",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Rejected Response",
"type": "main",
"index": 0
}
]
]
},
"Exact Match and Calculate": {
"main": [
[
{
"node": "Is Quote Ready",
"type": "main",
"index": 0
}
]
]
},
"Is Quote Ready": {
"main": [
[
{
"node": "Respond Quotable",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Needs Review",
"type": "main",
"index": 0
}
]
]
},
"Build Rejected Response": {
"main": [
[
{
"node": "Respond Rejected",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"nodeGroups": [],
"versionId": "f84b15d3-ac60-4dcf-b3f7-6b6501ccf240",
"activeVersionId": "f84b15d3-ac60-4dcf-b3f7-6b6501ccf240",
"sourceWorkflowId": null,
"tags": [],
"versionMetadata": {
"name": null,
"description": null
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
AUTOMATION NOTE Swiss Exact-Match Quote Prototype. Webhook trigger; 13 nodes.
Source: https://github.com/rains-hori/n8n-automation-recipes/blob/main/workflows/exact-match-quote/workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
A production-ready authentication workflow implementing secure user registration, login, token verification, and refresh token mechanisms. Perfect for adding authentication to any application without
Portfolio Orchestrator. Uses httpRequest. Webhook trigger; 59 nodes.
This n8n template demonstrates how a simple Multi-Layer Perceptron (MLP) neural network can predict housing prices. The prediction is based on four key features, processed through a three-layer model.
github code Try yourself
This workflow receives new consult bookings via webhook (Calendly v2 or a generic scheduling tool), generates timed confirmation and reminder messages, runs each SMS through a separate compliance work