This workflow corresponds to n8n.io template #12539 — we link there as the canonical source.
This workflow follows the Execute Workflow Trigger → HTTP Request recipe pattern — see all workflows that pair these two integrations.
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 →
{
"nodes": [
{
"id": "9b76fbe7-d643-4f54-b334-bfca4491f887",
"name": "Set Currencies",
"type": "n8n-nodes-base.set",
"position": [
176,
-304
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e8fe115d-61b3-44f3-93b5-04be7661804c",
"name": "trim",
"type": "boolean",
"value": false
},
{
"id": "1b14890b-8f18-4cbe-b7db-00d9107618cf",
"name": "baseCurrency",
"type": "string",
"value": "AED"
},
{
"id": "aa73f2d7-6452-499f-a60c-5e6c07d5e3e2",
"name": "currencies",
"type": "array",
"value": "[\"USD\", \"EUR\", \"GBP\", \"JPY\", \"CHF\", \"AUD\", \"CAD\", \"NZD\", \"CNY\", \"HKD\", \"SGD\", \"KRW\", \"TWD\", \"INR\", \"AED\", \"SAR\", \"QAR\", \"KWD\", \"BHD\", \"OMR\", \"SEK\", \"NOK\", \"DKK\", \"PLN\", \"CZK\", \"HUF\", \"RON\", \"ILS\", \"TRY\", \"ZAR\", \"BRL\", \"MXN\", \"CLP\", \"COP\", \"THB\", \"IDR\", \"MYR\", \"PHP\", \"VND\"]"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "b070066b-cf98-4e19-88e3-11028547d74e",
"name": "If Valid Data",
"type": "n8n-nodes-base.if",
"position": [
624,
-208
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "7f85de92-743b-4445-bc1b-3a626fe4e5fb",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.valid }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "cc9c9a7f-41d2-4825-9138-4bebf7d45a6a",
"name": "Stop and Error",
"type": "n8n-nodes-base.stopAndError",
"position": [
848,
-112
],
"parameters": {
"errorMessage": "=Error: {{ $json.reason }}"
},
"typeVersion": 1
},
{
"id": "77db9040-880b-4a9c-aa92-8cbb2eae8cc9",
"name": "Frankfurter",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
1296,
-304
],
"parameters": {
"url": "https://api.frankfurter.dev/v1/latest",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "base",
"value": "={{ $json.fx.base }}"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.3
},
{
"id": "f2e4e674-d014-41df-9b14-7520e7dd40e5",
"name": "Normalize Frankfurter",
"type": "n8n-nodes-base.code",
"position": [
1744,
-400
],
"parameters": {
"jsCode": "const res = $input.first().json;\n\nif (!res || typeof res !== 'object' || !res.rates) {\n return [{\n json: {\n ok: false,\n provider: 'frankfurter',\n error: 'invalid_response'\n }\n }];\n}\n\nreturn [{\n json: {\n ok: true,\n provider: 'frankfurter',\n base: (res.base || '').toUpperCase(),\n asOf: res.date || null,\n rates: res.rates\n }\n}];"
},
"typeVersion": 2
},
{
"id": "bdb88e3b-d12a-46eb-ae4b-5a308173d12c",
"name": "open.er-api.com",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
2416,
-224
],
"parameters": {
"url": "=https://open.er-api.com/v6/latest/{{$json.fx.base}}",
"options": {}
},
"retryOnFail": true,
"typeVersion": 4.3
},
{
"id": "c327f7b4-1547-4c89-a2e6-98895601048d",
"name": "If base correct 1",
"type": "n8n-nodes-base.if",
"position": [
1520,
-304
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "30c6550f-e8b8-461c-94ad-c4acae27a08d",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.base }}",
"rightValue": "={{ $('Initialize Coverage Tracking').first().json.fx.base }}"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "585cb49c-716d-41bc-8f30-eae30b2e50b4",
"name": "If base correct 2",
"type": "n8n-nodes-base.if",
"position": [
2640,
-224
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "30c6550f-e8b8-461c-94ad-c4acae27a08d",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.base_code }}",
"rightValue": "={{ $('Merge Rates & Check Coverage (1)').first().json.fx.base }}"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "930d6e7c-1d0b-457a-a396-cf6b83c467d7",
"name": "Fetch FX Rates",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
176,
-112
],
"parameters": {
"workflowInputs": {
"values": [
{
"name": "trim",
"type": "boolean"
},
{
"name": "baseCurrency"
},
{
"name": "currencies",
"type": "array"
}
]
}
},
"typeVersion": 1.1
},
{
"id": "4a5f1c28-820c-49b6-be4b-7cee3f68b897",
"name": "Stop and Error3",
"type": "n8n-nodes-base.stopAndError",
"position": [
3536,
-176
],
"parameters": {
"errorMessage": "Unable to fetch all currencies."
},
"typeVersion": 1
},
{
"id": "ef99aeb2-f5a0-472d-8a67-2575454e27fd",
"name": "Normalize open.er-api.com",
"type": "n8n-nodes-base.code",
"position": [
2864,
-320
],
"parameters": {
"jsCode": "const res = $input.first().json;\n\nif (!res || res.result !== 'success' || !res.rates) {\n return [{\n json: {\n ok: false,\n provider: 'open.er-api.com',\n error: res?.error_type || 'invalid_response'\n }\n }];\n}\n\nreturn [{\n json: {\n ok: true,\n provider: 'open.er-api.com',\n base: (res.base_code || '').toUpperCase(),\n asOf: res.time_last_update_utc || res.time_last_update_unix || null,\n rates: res.rates\n }\n}];"
},
"typeVersion": 2
},
{
"id": "e170996f-5970-46ce-8bf1-8de7596e3cc5",
"name": "Handle Wrong Base 1",
"type": "n8n-nodes-base.code",
"position": [
1744,
-208
],
"parameters": {
"jsCode": "return [{\n json: {\n ok: false,\n provider: 'frankfurter',\n error: 'invalid_basecurrency'\n }\n}];"
},
"typeVersion": 2
},
{
"id": "e2e8d53f-a585-4a1c-b1f6-fae93fbe1f92",
"name": "Handle Wrong Base 2",
"type": "n8n-nodes-base.code",
"position": [
2864,
-128
],
"parameters": {
"jsCode": "return [{\n json: {\n ok: false,\n provider: 'open.er-api.com',\n error: 'invalid_basecurrency'\n }\n}];"
},
"typeVersion": 2
},
{
"id": "9f4e28ec-480c-4581-8d86-b6a4b6d47502",
"name": "Trim",
"type": "n8n-nodes-base.code",
"position": [
3760,
-464
],
"parameters": {
"jsCode": "// Source of truth: Create List\nconst req = $('Validate & Normalize Inputs').first().json;\n\nconst requestedBase = String(req.baseCurrency).toUpperCase();\nconst requestedCurrencies = Array.isArray(req.currencies)\n ? req.currencies.map(c => String(c).toUpperCase())\n : [];\n\n// Current FX payload\nconst fx = $input.first().json.fx || {};\nconst rates = fx.rates || {};\nconst sources = fx.sources || {};\n\n// Prepare trimmed objects\nconst trimmedRates = {};\nconst trimmedSources = {};\n\n// Always include base\ntrimmedRates[requestedBase] = 1;\nif (sources[requestedBase]) {\n trimmedSources[requestedBase] = sources[requestedBase];\n}\n\n// Include only requested currencies\nfor (const sym of requestedCurrencies) {\n if (typeof rates[sym] === 'number') {\n trimmedRates[sym] = rates[sym];\n if (sources[sym]) {\n trimmedSources[sym] = sources[sym];\n }\n }\n}\n\nreturn [\n {\n json: {\n ...$input.first().json,\n fx: {\n base: requestedBase,\n asOf: fx.asOf || null,\n rates: trimmedRates,\n sources: trimmedSources\n }\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "f22fcecd-6344-44af-a487-b82dc34e1498",
"name": "If Trim",
"type": "n8n-nodes-base.if",
"position": [
3536,
-400
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d99e26e3-7469-4528-a0c6-82aa4737177c",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $('Validate & Normalize Inputs').first().json.trim }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "ec7a2f04-a6c8-47b0-bf16-2ceb0fd7eb62",
"name": "Manual Trigger (Example)",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-48,
-304
],
"parameters": {},
"typeVersion": 1
},
{
"id": "bf019fea-6cc2-4076-b16b-a6cf4c051d6c",
"name": "Validate & Normalize Inputs",
"type": "n8n-nodes-base.code",
"position": [
400,
-208
],
"parameters": {
"jsCode": "// Get base currency from trigger\nconst trim = $input.first().json.trim;\nconst baseCurrencyRaw = $input.first().json.baseCurrency;\nconst baseCurrency = baseCurrencyRaw?.toUpperCase();\n\nconst currenciesRaw = $input.first().json.currencies;\n\n// Preparing return object\nlet object = {\n valid: false,\n reason: '',\n trim,\n baseCurrency,\n currencies: currenciesRaw\n};\n\nif (!baseCurrency || typeof baseCurrency !== 'string') {\n object.reason = 'baseCurrency is missing or invalid';\n} else if (!Array.isArray(currenciesRaw)) {\n object.reason = 'currencies is not an array';\n} else {\n // Check presence\n const currencies = currenciesRaw.map(c => c.toUpperCase());\n const hasBase = currencies.includes(baseCurrency);\n const filteredCurrencies = hasBase\n ? currencies.filter((c) => c !== baseCurrency)\n : currencies;\n object.valid = true;\n object.currencies = filteredCurrencies;\n}\n\nreturn [\n {\n json: object\n }\n];"
},
"typeVersion": 2
},
{
"id": "528a4062-c9a8-4924-b859-3b5aa377ebc1",
"name": "Initialize FX State + Static Rates",
"type": "n8n-nodes-base.code",
"position": [
848,
-304
],
"parameters": {
"jsCode": "const base = $input.first().json.baseCurrency?.toUpperCase();\n\nconst staticRates = {};\nconst staticMeta = {};\n\n// only apply when base is USD\nif (base === 'USD') {\n // optional\n // staticRates.AED = 3.6725;\n // staticMeta.AED = 'static_usd_peg';\n // staticRates.SAR = 3.75;\n // staticMeta.SAR = 'static_usd_peg';\n // staticRates.QAR = 3.64;\n // staticMeta.QAR = 'static_usd_peg';\n}\n\nreturn [{\n json: {\n ...$input.first().json,\n fx: {\n base,\n asOf: new Date().toISOString(),\n rates: staticRates,\n sources: staticMeta\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "030de89f-2d67-425f-819b-5f7daf12a696",
"name": "Initialize Coverage Tracking",
"type": "n8n-nodes-base.code",
"position": [
1072,
-304
],
"parameters": {
"jsCode": "function isRate(x) {\n return typeof x === 'number' && Number.isFinite(x) && x > 0;\n}\n\nconst req = $('Validate & Normalize Inputs').first().json; // source of truth\nconst requestedBase = req.baseCurrency; // already uppercase\nconst requestedSymbols = req.currencies || []; // already excludes base\n\nconst runningFx = $input.first().json.fx || { base: requestedBase, rates: {}, sources: {}, asOf: null };\nconst rates = runningFx.rates || {};\n\nconst missing = [];\nconst invalid = [];\n\nfor (const sym of requestedSymbols) {\n const v = rates[sym];\n if (v == null) missing.push(sym);\n else if (!isRate(v)) invalid.push(sym);\n}\n\nreturn [{\n json: {\n fx: {\n base: requestedBase,\n asOf: runningFx.asOf || new Date().toISOString(),\n rates: rates,\n sources: runningFx.sources || {}\n },\n missing,\n invalid,\n complete: missing.length === 0 && invalid.length === 0\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "c8922d5e-2baf-4e5e-b29e-02a9596f4608",
"name": "Merge Rates & Check Coverage (1)",
"type": "n8n-nodes-base.code",
"position": [
1968,
-304
],
"parameters": {
"jsCode": "function isRate(x) {\n return typeof x === 'number' && Number.isFinite(x) && x > 0;\n}\n\nconst requestedBase = $('Validate & Normalize Inputs').first().json.baseCurrency.toUpperCase();\nconst requestedSymbols = $('Validate & Normalize Inputs').first().json.currencies; // already excludes base\n\nconst runningFx = $('Initialize Coverage Tracking').first().json.fx;\nconst provider = $input.first().json; // normalized provider output\n\n// start with running\nconst mergedRates = { ...(runningFx.rates || {}) };\nconst mergedSources = { ...(runningFx.sources || {}) };\n\n// only merge provider if ok and base matches\nif (provider.ok && provider.base === requestedBase && provider.rates && typeof provider.rates === 'object') {\n for (const [k, v] of Object.entries(provider.rates)) {\n const sym = k.toUpperCase();\n if (!isRate(v)) continue;\n\n // do not overwrite existing rate (static wins if already set)\n if (mergedRates[sym] == null) {\n mergedRates[sym] = v;\n mergedSources[sym] = provider.provider;\n }\n }\n}\n\n// now check coverage\nconst missing = [];\nconst invalid = [];\n\nfor (const sym of requestedSymbols) {\n const v = mergedRates[sym];\n if (v == null) missing.push(sym);\n else if (!isRate(v)) invalid.push(sym);\n}\n\nconst complete = missing.length === 0 && invalid.length === 0;\n\nreturn [{\n json: {\n fx: {\n base: requestedBase,\n asOf: provider.asOf || runningFx.asOf,\n rates: mergedRates,\n sources: mergedSources\n },\n complete,\n missing,\n invalid\n }\n}];"
},
"typeVersion": 2
},
{
"id": "4cd57cab-a81c-4a2a-8594-bc5bf062b91c",
"name": "Merge Rates & Check Coverage (2)",
"type": "n8n-nodes-base.code",
"position": [
3088,
-224
],
"parameters": {
"jsCode": "function isRate(x) {\n return typeof x === 'number' && Number.isFinite(x) && x > 0;\n}\n\nconst requestedBase = $('Validate & Normalize Inputs').first().json.baseCurrency.toUpperCase();\nconst requestedSymbols = $('Validate & Normalize Inputs').first().json.currencies; // already excludes base\n\nconst runningFx = $('Merge Rates & Check Coverage (1)').first().json.fx || { base: requestedBase, rates: {}, sources: {} };\nconst provider = $input.first().json; // normalized provider output\n\n// start with running\nconst mergedRates = { ...(runningFx.rates || {}) };\nconst mergedSources = { ...(runningFx.sources || {}) };\n\n// only merge provider if ok and base matches\nif (provider.ok && provider.base === requestedBase && provider.rates && typeof provider.rates === 'object') {\n for (const [k, v] of Object.entries(provider.rates)) {\n const sym = k.toUpperCase();\n if (!isRate(v)) continue;\n\n // do not overwrite existing rate (static wins if already set)\n if (mergedRates[sym] == null) {\n mergedRates[sym] = v;\n mergedSources[sym] = provider.provider;\n }\n }\n}\n\n// now check coverage\nconst missing = [];\nconst invalid = [];\n\nfor (const sym of requestedSymbols) {\n const v = mergedRates[sym];\n if (v == null) missing.push(sym);\n else if (!isRate(v)) invalid.push(sym);\n}\n\nconst complete = missing.length === 0 && invalid.length === 0;\n\nreturn [{\n json: {\n fx: {\n base: requestedBase,\n asOf: provider.asOf || runningFx.asOf,\n rates: mergedRates,\n sources: mergedSources\n },\n complete,\n missing,\n invalid\n }\n}];"
},
"typeVersion": 2
},
{
"id": "c357635b-f4e8-4c6b-819c-f79442792da3",
"name": "Coverage Complete 1?",
"type": "n8n-nodes-base.if",
"position": [
2192,
-304
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "36c63465-3ccb-4ab4-a43b-785468cdefa0",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.complete }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "77e273fc-fc8d-4716-8192-366f855c7a26",
"name": "Coverage Complete 2?",
"type": "n8n-nodes-base.if",
"position": [
3312,
-224
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "36c63465-3ccb-4ab4-a43b-785468cdefa0",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.complete }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "9072a67f-0e25-4d4a-9d06-04e2d0027ec7",
"name": "Final Output",
"type": "n8n-nodes-base.noOp",
"position": [
3984,
-400
],
"parameters": {},
"typeVersion": 1
},
{
"id": "5a14b24b-277d-40b5-b1be-f117d56c6330",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-240,
-1424
],
"parameters": {
"width": 580,
"height": 894,
"content": "# Multi-provider FX Rates Fetcher\nThis workflow fetches foreign exchange (FX) rates for a configurable base currency using multiple public APIs.\n\nIt is designed to guarantee complete coverage for the requested currencies. The workflow validates all inputs, queries providers sequentially, and merges results into a single enforced output schema. If full coverage cannot be achieved, the workflow fails instead of returning partial data.\n\nAn optional trim flag allows the final output to be limited to only the currencies originally requested.\n\n## How it works\n1. The workflow starts by validating inputs (`baseCurrency` and `currencies`), it removes the base currency from the target list and ensure that the input is valid.\n2. An internal FX is then initialized. This state acts as the single source of truth and can optionally include static rates, which always take precedence over provider data.\n3. Rates are fetched from a primary provider and merged into the FX state if the response base currency matches the requested base. Coverage is checked after each provider call. If all requested currencies are resolved, the workflow stops early.\n4. If coverage is incomplete, a secondary provider is queried for the remaining currencies. If coverage is still incomplete after the fallback provider, the workflow fails.\n5. If coverage is complete, the workflow optional.\n\n## Setup\n* [ ] Provide `baseCurrency` (ISO code) and `currencies` (array of ISO codes).\n* [ ] Enable the `trim` option if you want the output limited to requested currencies only.\n* [ ] Run manually or replace the trigger with a schedule, webhook, or another workflow.\n* [ ] Edit the static rates block if you need to inject fixed peg rates (optional).\n\n## Requirements\n* None. The workflow uses public APIs and does not require authentication or paid services."
},
"typeVersion": 1
},
{
"id": "b97fd36c-9e86-4bfa-8f1d-5a15ce47c232",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-240,
-464
],
"parameters": {
"color": 3,
"width": 576,
"height": 496,
"content": "## Inputs\n* `trim` (boolean)\n* `baseCurrency` (string, e.g. \"EUR\")\n* `currencies` (array of string ISO currency codes, e.g [\"USD\",\"EUR\",\"IDR\",\"AED\"])\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n### Use Manual Trigger or\n### Call from another workflow"
},
"typeVersion": 1
},
{
"id": "624b0a2b-ff5b-4e49-83cc-2ce2b9b000a5",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
752,
-560
],
"parameters": {
"color": 6,
"width": 288,
"height": 416,
"content": "## Optional static rates\n* You can define fixed FX rates here. Static rates always override provider results.\n\ne.g. \nif (base === 'USD') {\n staticRates.AED = 3.6725;\n staticMeta.AED = 'static_usd_peg';\n}"
},
"typeVersion": 1
},
{
"id": "f848ff8c-83d2-4622-aeee-64a4f1cd3e82",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
2352,
-464
],
"parameters": {
"color": 6,
"width": 1120,
"height": 512,
"content": "## Optional expand providers\n* Clone this nodes to expand your fetcher with another provider and connect the clones to Stop and Error3 node."
},
"typeVersion": 1
}
],
"connections": {
"Trim": {
"main": [
[
{
"node": "Final Output",
"type": "main",
"index": 0
}
]
]
},
"If Trim": {
"main": [
[
{
"node": "Trim",
"type": "main",
"index": 0
}
],
[
{
"node": "Final Output",
"type": "main",
"index": 0
}
]
]
},
"Frankfurter": {
"main": [
[
{
"node": "If base correct 1",
"type": "main",
"index": 0
}
]
]
},
"If Valid Data": {
"main": [
[
{
"node": "Initialize FX State + Static Rates",
"type": "main",
"index": 0
}
],
[
{
"node": "Stop and Error",
"type": "main",
"index": 0
}
]
]
},
"Fetch FX Rates": {
"main": [
[
{
"node": "Validate & Normalize Inputs",
"type": "main",
"index": 0
}
]
]
},
"Set Currencies": {
"main": [
[
{
"node": "Validate & Normalize Inputs",
"type": "main",
"index": 0
}
]
]
},
"open.er-api.com": {
"main": [
[
{
"node": "If base correct 2",
"type": "main",
"index": 0
}
]
]
},
"If base correct 1": {
"main": [
[
{
"node": "Normalize Frankfurter",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Wrong Base 1",
"type": "main",
"index": 0
}
]
]
},
"If base correct 2": {
"main": [
[
{
"node": "Normalize open.er-api.com",
"type": "main",
"index": 0
}
],
[
{
"node": "Handle Wrong Base 2",
"type": "main",
"index": 0
}
]
]
},
"Handle Wrong Base 1": {
"main": [
[
{
"node": "Merge Rates & Check Coverage (1)",
"type": "main",
"index": 0
}
]
]
},
"Handle Wrong Base 2": {
"main": [
[
{
"node": "Merge Rates & Check Coverage (2)",
"type": "main",
"index": 0
}
]
]
},
"Coverage Complete 1?": {
"main": [
[
{
"node": "If Trim",
"type": "main",
"index": 0
}
],
[
{
"node": "open.er-api.com",
"type": "main",
"index": 0
}
]
]
},
"Coverage Complete 2?": {
"main": [
[
{
"node": "If Trim",
"type": "main",
"index": 0
}
],
[
{
"node": "Stop and Error3",
"type": "main",
"index": 0
}
]
]
},
"Normalize Frankfurter": {
"main": [
[
{
"node": "Merge Rates & Check Coverage (1)",
"type": "main",
"index": 0
}
]
]
},
"Manual Trigger (Example)": {
"main": [
[
{
"node": "Set Currencies",
"type": "main",
"index": 0
}
]
]
},
"Normalize open.er-api.com": {
"main": [
[
{
"node": "Merge Rates & Check Coverage (2)",
"type": "main",
"index": 0
}
]
]
},
"Validate & Normalize Inputs": {
"main": [
[
{
"node": "If Valid Data",
"type": "main",
"index": 0
}
]
]
},
"Initialize Coverage Tracking": {
"main": [
[
{
"node": "Frankfurter",
"type": "main",
"index": 0
}
]
]
},
"Merge Rates & Check Coverage (1)": {
"main": [
[
{
"node": "Coverage Complete 1?",
"type": "main",
"index": 0
}
]
]
},
"Merge Rates & Check Coverage (2)": {
"main": [
[
{
"node": "Coverage Complete 2?",
"type": "main",
"index": 0
}
]
]
},
"Initialize FX State + Static Rates": {
"main": [
[
{
"node": "Initialize Coverage Tracking",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
It validates all inputs, queries providers sequentially, and merges results into a single enforced output schema. The workflow is designed to guarantee complete coverage for the requested currencies. If full coverage cannot be achieved, it fails explicitly instead of returning…
Source: https://n8n.io/workflows/12539/ — 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.
Upload files from any source to your account Kommo or AmoCRM with a simple and reusable workflow. It can split a large file into small ones and upload chunks. Works for Kommo and amoCRM There are 3 re
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This workflow is designed to translate a video accessible by URL (supported sources: YouTube, Google Drive, S3, Vimeo, or a direct link) into a language supported by Rask AI.
This workflow integrates the Apache Airflow API DAGRun and XCom. It enables n8n to trigger Airflow DAGs and retrieve the execution results. Update Airflow API Link Prefix Navigate to the node. Update
[n8n] Advanced URL Parsing and Shortening Workflow - Switchy.io Integration. Uses splitInBatches, stickyNote, httpRequest, html. Event-driven trigger; 56 nodes.