Zapier/Make to n8n Migration Kit
Module 23 · ship · advanced
learner.json
In n8n, copy this file and paste it onto the canvas, or use Import from File.
{
"name": "23 · Zapier/Make to n8n Migration Kit",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "mig/convert",
"responseMode": "responseNode",
"options": {}
},
"id": "m23-convert-api",
"name": "Convert API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"webhookId": "course-23-convert"
},
{
"parameters": {
"httpMethod": "GET",
"path": "mig/mappings",
"responseMode": "responseNode",
"options": {}
},
"id": "m23-mappings-api",
"name": "Mappings API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
400
],
"webhookId": "course-23-mappings"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "s1",
"name": "app_map",
"value": "={{ {\"webhook\": {\"trigger\": [\"n8n-nodes-base.webhook\",2],\"action\": [\"n8n-nodes-base.httpRequest\",4.2]},\"http\": {\"action\": [\"n8n-nodes-base.httpRequest\",4.2]},\"schedule\": {\"trigger\": [\"n8n-nodes-base.scheduleTrigger\",1.2]},\"gmail\": {\"trigger\": [\"n8n-nodes-base.gmailTrigger\",1.3],\"action\": [\"n8n-nodes-base.gmail\",2.1],\"credential\": \"Gmail\"},\"googlesheets\": {\"trigger\": [\"n8n-nodes-base.googleSheetsTrigger\",1],\"action\": [\"n8n-nodes-base.googleSheets\",4.5],\"credential\": \"Google Sheets\"},\"googledrive\": {\"trigger\": [\"n8n-nodes-base.googleDriveTrigger\",1],\"action\": [\"n8n-nodes-base.googleDrive\",3],\"credential\": \"Google Drive\"},\"googlecalendar\": {\"trigger\": [\"n8n-nodes-base.googleCalendarTrigger\",1],\"action\": [\"n8n-nodes-base.googleCalendar\",1.3],\"credential\": \"Google Calendar\"},\"slack\": {\"trigger\": [\"n8n-nodes-base.slackTrigger\",1],\"action\": [\"n8n-nodes-base.slack\",2.3],\"credential\": \"Slack\"},\"hubspot\": {\"trigger\": [\"n8n-nodes-base.hubspotTrigger\",1],\"action\": [\"n8n-nodes-base.hubspot\",2.1],\"credential\": \"HubSpot\"},\"airtable\": {\"trigger\": [\"n8n-nodes-base.airtableTrigger\",1],\"action\": [\"n8n-nodes-base.airtable\",2.1],\"credential\": \"Airtable\"},\"notion\": {\"trigger\": [\"n8n-nodes-base.notionTrigger\",1],\"action\": [\"n8n-nodes-base.notion\",2.2],\"credential\": \"Notion\"},\"stripe\": {\"trigger\": [\"n8n-nodes-base.stripeTrigger\",1],\"action\": [\"n8n-nodes-base.stripe\",1],\"credential\": \"Stripe\"},\"shopify\": {\"trigger\": [\"n8n-nodes-base.shopifyTrigger\",1],\"action\": [\"n8n-nodes-base.shopify\",1],\"credential\": \"Shopify\"},\"typeform\": {\"trigger\": [\"n8n-nodes-base.typeformTrigger\",1],\"credential\": \"Typeform\"},\"calendly\": {\"trigger\": [\"n8n-nodes-base.calendlyTrigger\",1],\"credential\": \"Calendly\"},\"telegram\": {\"trigger\": [\"n8n-nodes-base.telegramTrigger\",1.1],\"action\": [\"n8n-nodes-base.telegram\",1.2],\"credential\": \"Telegram\"},\"discord\": {\"action\": [\"n8n-nodes-base.discord\",2],\"credential\": \"Discord\"},\"trello\": {\"trigger\": [\"n8n-nodes-base.trelloTrigger\",1],\"action\": [\"n8n-nodes-base.trello\",1],\"credential\": \"Trello\"},\"mailchimp\": {\"trigger\": [\"n8n-nodes-base.mailchimpTrigger\",1],\"action\": [\"n8n-nodes-base.mailchimp\",1],\"credential\": \"Mailchimp\"},\"pipedrive\": {\"trigger\": [\"n8n-nodes-base.pipedriveTrigger\",1],\"action\": [\"n8n-nodes-base.pipedrive\",1],\"credential\": \"Pipedrive\"},\"openai\": {\"action\": [\"@n8n/n8n-nodes-langchain.openAi\",1.8],\"credential\": \"OpenAI\"},\"setvariable\": {\"action\": [\"n8n-nodes-base.set\",3.4]},\"delay\": {\"action\": [\"n8n-nodes-base.wait\",1.1]},\"code\": {\"action\": [\"n8n-nodes-base.code\",2]} } }}",
"type": "object"
},
{
"id": "s2",
"name": "app_aliases",
"value": "={{ {\"gateway\": \"webhook\",\"webhooks\": \"webhook\",\"catchhook\": \"webhook\",\"googleemail\": \"gmail\",\"googlemail\": \"gmail\",\"googlesheet\": \"googlesheets\",\"hubspotcrm\": \"hubspot\",\"openaigpt3\": \"openai\",\"chatgpt\": \"openai\",\"util\": \"setvariable\",\"json\": \"code\",\"formatter\": \"code\",\"regexp\": \"code\",\"javascript\": \"code\",\"sleep\": \"delay\",\"scheduler\": \"schedule\"} }}",
"type": "object"
},
{
"id": "s3",
"name": "hours_mapped",
"value": 0.25,
"type": "number"
},
{
"id": "s4",
"name": "hours_partial",
"value": 0.75,
"type": "number"
},
{
"id": "s5",
"name": "hours_manual",
"value": 2,
"type": "number"
},
{
"id": "s6",
"name": "hours_testing",
"value": 1,
"type": "number"
},
{
"id": "s7",
"name": "hourly_rate",
"value": 75,
"type": "number"
},
{
"id": "s8",
"name": "zapier_cost_per_1000_tasks",
"value": 30,
"type": "number"
},
{
"id": "s9",
"name": "make_cost_per_10000_ops",
"value": 11,
"type": "number"
},
{
"id": "s10",
"name": "n8n_monthly_cost",
"value": 20,
"type": "number"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "m23-settings-edit-me",
"name": "Settings (edit me)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
240,
200
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r1",
"leftValue": "={{ $('Convert API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "convert"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r2",
"leftValue": "={{ $('Mappings API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "mappings"
}
]
},
"options": {}
},
"id": "m23-route",
"name": "Route",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
460,
200
]
},
{
"parameters": {
"jsCode": "// Read a Make blueprint or a Zapier export and turn every scenario/zap into the same step tree:\n// { key, label, app, action, kind: trigger|action|router|iterator|aggregator, params, filter, routes }\nconst body = $('Convert API').first().json.body ?? {};\nconst parse = (v) => (typeof v === 'string' ? JSON.parse(v) : v);\nconst appKey = (s) => String(s ?? '').replace(/@.*$/, '').replace(/API$/i, '').replace(/V\\d+$/i, '').toLowerCase().replace(/[^a-z0-9]/g, '');\n\n// --- Make: blueprint.flow[] with module \"app:action\", mapper/parameters, filter on the module, router routes\nfunction makeSteps(flow, isRoot) {\n return (flow ?? []).map((m, i) => {\n const [app, action] = String(m.module ?? '').split(':');\n const kind = m.module === 'builtin:BasicRouter' ? 'router' : m.module === 'builtin:BasicFeeder' ? 'iterator'\n : /Aggregator$/.test(action ?? '') ? 'aggregator' : isRoot && i === 0 ? 'trigger' : 'action';\n return {\n key: String(m.id), label: m.metadata?.designer?.name || m.label || `${app} ${action}`,\n app: appKey(app), app_raw: app, action: action ?? '', kind,\n params: { ...(m.parameters ?? {}), ...(m.mapper ?? {}) },\n filter: m.filter ? { label: m.filter.name ?? 'Filter', groups: (m.filter.conditions ?? []).map((g) => g.map((c) => ({ a: c.a, op: c.o, b: c.b }))) } : null,\n routes: kind === 'router' ? (m.routes ?? []).map((r, j) => ({ label: r.flow?.[0]?.filter?.name || `Route ${j + 1}`, steps: makeSteps(r.flow, false) })) : null,\n };\n });\n}\n\n// --- Zapier: zapfile { zaps: [{ title, nodes: { id: { selected_api, action, type_of, params, parent_id } } }] }\nconst zapOp = { exact: 'text:equal', iexact: 'text:equal', contains: 'text:contain', icontains: 'text:contain', gt: 'number:greater', lt: 'number:less', exists: 'exist', not_exists: 'notexist', is_true: 'boolean:true', is_false: 'boolean:false' };\nfunction zapSteps(nodes) {\n const all = Object.values(nodes ?? {});\n const kids = (id) => all.filter((n) => String(n.parent_id ?? '') === String(id ?? '')).sort((a, b) => Number(a.id) - Number(b.id));\n const filterOf = (n) => ({ label: n.title || 'Filter', groups: (n.params?.filter ?? n.params?.filter_criteria ?? []).map((g) => g.map((c) => ({ a: c.key, op: zapOp[c.match] ?? c.match, b: c.value }))) });\n function chain(start, isRoot) {\n const out = [];\n let pendingFilter = null;\n for (let n = start, i = 0; n; i++) {\n const app = appKey(n.selected_api);\n if (app === 'filter') { pendingFilter = filterOf(n); n = kids(n.id)[0]; continue; }\n const children = kids(n.id);\n const isPaths = /^(branching|paths)/.test(app);\n out.push({\n key: String(n.id), label: n.title || `${app} ${n.action ?? ''}`.trim(), app, app_raw: n.selected_api, action: n.action ?? '',\n kind: isPaths ? 'router' : n.type_of === 'read' && isRoot && i === 0 ? 'trigger' : 'action',\n params: n.params ?? {}, filter: pendingFilter,\n routes: isPaths ? children.map((c) => ({ label: c.title || 'Path', filter: c.params?.filter ? filterOf(c) : null, steps: chain(kids(c.id)[0], false) })) : null,\n });\n pendingFilter = null;\n if (isPaths) break;\n n = children[0];\n }\n return out;\n }\n return chain(kids(null)[0] ?? all.find((n) => !n.parent_id), true);\n}\n\nlet scenarios = [], error = null;\ntry {\n if (body.blueprint || body.blueprints) {\n for (const bp of (body.blueprints ?? [body.blueprint]).map(parse))\n scenarios.push({ name: bp.name || 'Make scenario', source: 'make', steps: makeSteps(bp.flow, true) });\n } else if (body.zapfile || body.zap) {\n const zf = parse(body.zapfile ?? { zaps: [parse(body.zap)] });\n for (const z of zf.zaps ?? []) scenarios.push({ name: z.title || `Zap ${z.id}`, source: 'zapier', steps: zapSteps(z.nodes) });\n } else error = 'send \"blueprint\" (Make export), \"blueprints\" (array) or \"zapfile\" / \"zap\" (Zapier export)';\n} catch (e) { error = `could not read the export: ${e.message}`; }\nif (!error && !scenarios.some((s) => s.steps.length)) error = 'no steps found in the export';\n\nconst runs = body.monthly_runs ?? {};\nreturn [{ json: { ok: !error, error, scenarios: scenarios.map((s) => ({ ...s, monthly_runs: Number(typeof runs === 'number' ? runs : runs[s.name] ?? 1000) })),\n create: body.create === true, dry_run: body.dry_run === true } }];\n"
},
"id": "m23-read-export",
"name": "Read Export",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
0
]
},
{
"parameters": {
"jsCode": "// Turn each scenario's step tree into an n8n workflow (nodes, connections, layout) and record how well\n// every step mapped: \"mapped\" (ready), \"partial\" (right node, check settings) or \"manual\" (no equivalent).\nconst s = $('Settings (edit me)').first().json;\nconst APPS = s.app_map;\nconst ALIAS = s.app_aliases;\n\n// Small deterministic hash (no crypto in the Code sandbox) for stable workflow/webhook ids\nconst hash = (str, len) => { let h1 = 0xdeadbeef, h2 = 0x41c6ce57; for (const ch of str) { const c = ch.charCodeAt(0); h1 = Math.imul(h1 ^ c, 2654435761); h2 = Math.imul(h2 ^ c, 1597334677); }\n return ((h1 >>> 0).toString(36) + (h2 >>> 0).toString(36) + '0000000000000000').slice(0, len); };\nconst slug = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'hook';\n\nfunction convert(sc) {\n const nodes = [], connections = {}, rows = [], used = new Set(), byKey = {}, viaBody = {};\n const uniq = (name) => { let n = name.slice(0, 60), i = 2; while (used.has(n)) n = `${name.slice(0, 56)} ${i++}`; used.add(n); return n; };\n const link = (from, to, out = 0) => {\n const c = (connections[from] ??= { main: [] }).main; while (c.length <= out) c.push([]);\n c[out].push({ node: to, type: 'main', index: 0 });\n };\n\n // Rewrite Make {{3.email}} / Zapier {{123__email}} references to n8n expressions.\n const flags = [];\n function tx(v) {\n if (Array.isArray(v)) return v.map(tx);\n if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, tx(x)]));\n if (typeof v !== 'string' || !v.includes('{{')) return v;\n const ref = (key, path) => { const name = byKey[key]; if (!name) { flags.push(`unknown step reference ${key}`); return null; }\n if (viaBody[key]) path = 'body.' + path; // n8n's Webhook node puts the request body under .body\n return `$('${name}').item.json${path.split(/[.]/).map((p) => (/^[A-Za-z_]\\w*$/.test(p) ? '.' + p : `['${p}']`)).join('')}`; };\n let out = v\n .replace(/\\{\\{\\s*(\\d+)\\.([\\w.]+)\\s*\\}\\}/g, (m, k, p) => { const r = ref(k, p); return r ? `{{ ${r} }}` : m; }) // Make\n .replace(/\\{\\{\\s*(\\d+)__([\\w]+?)\\s*\\}\\}/g, (m, k, p) => { const r = ref(k, p.replace(/__/g, '.')); return r ? `{{ ${r} }}` : m; }); // Zapier\n const leftover = out.match(/\\{\\{(?! \\$\\(')[^}]*\\}\\}/g);\n if (leftover) flags.push(`translate by hand: ${leftover.slice(0, 2).join(' ')}`);\n return '=' + out;\n }\n\n // n8n parameters for the node types we know how to fill in. Returns [params, fullyMapped].\n function paramsFor(type, st) {\n const p = tx(st.params);\n const pick = (...ks) => { for (const k of ks) if (p[k] !== undefined && p[k] !== '') return p[k]; return undefined; };\n const list = (x) => (Array.isArray(x) ? x.map((e) => (typeof e === 'object' ? e.address ?? e.email ?? JSON.stringify(e) : e)).join(',') : x);\n switch (type) {\n case 'n8n-nodes-base.webhook': return [{ httpMethod: 'POST', path: `migrated/${slug(st.label)}`, options: {} }, true];\n case 'n8n-nodes-base.scheduleTrigger': return [{ rule: { interval: [{ field: 'minutes', minutesInterval: Number(st.params.interval ?? 15) }] } }, true];\n case 'n8n-nodes-base.gmail': { const to = list(pick('to', 'To', 'recipients')); const subject = pick('subject', 'Subject'); const message = pick('content', 'html', 'body', 'Body', 'text');\n return [{ sendTo: to ?? '', subject: subject ?? '', emailType: 'text', message: message ?? '', options: {} }, Boolean(to && subject && message)]; }\n case 'n8n-nodes-base.slack': { const channel = pick('channel', 'channelId'); const text = pick('text', 'message');\n return [{ resource: 'message', operation: 'post', select: 'channel', channelId: { __rl: true, mode: 'name', value: String(channel ?? '').replace(/^=/, '') }, text: text ?? '', otherOptions: {} }, Boolean(channel && text)]; }\n case 'n8n-nodes-base.httpRequest': { const url = pick('url', 'URL'); const method = String(pick('method') ?? 'GET').replace(/^=/, '').toUpperCase();\n return [{ method, url: url ?? '', options: {} }, Boolean(url) && !pick('data', 'body')]; }\n case 'n8n-nodes-base.googleSheets': { const doc = pick('spreadsheetId', 'spreadsheet'); const sheet = pick('sheetId', 'sheet', 'worksheet');\n return [{ operation: 'append', documentId: { __rl: true, mode: 'id', value: String(doc ?? '').replace(/^=/, '') }, sheetName: { __rl: true, mode: 'name', value: String(sheet ?? '').replace(/^=/, '') },\n columns: { mappingMode: 'defineBelow', value: p.values ?? p.row ?? {} }, options: {} }, false]; } // column mapping always needs a look\n case 'n8n-nodes-base.set': { const vars = p.variables ?? (p.name ? [{ name: p.name, value: p.value }] : []);\n return [{ assignments: { assignments: vars.map((x, i) => ({ id: `a${i}`, name: String(x.name).replace(/^=/, ''), value: x.value, type: 'string' })) }, options: {} }, vars.length > 0]; }\n case 'n8n-nodes-base.wait': return [{ amount: Number(st.params.duration ?? st.params.delay_for ?? 1), unit: st.params.unit ?? 'minutes' }, Boolean(st.params.duration ?? st.params.delay_for)];\n case 'n8n-nodes-base.code': return [{ jsCode: `// Migrated from ${st.app_raw} ${st.action}: rewrite this step in JavaScript.\\n// Original settings:\\n${JSON.stringify(st.params, null, 2).split('\\n').map((l) => '// ' + l).join('\\n')}\\nreturn $input.all();` }, false];\n default: return [{}, false];\n }\n }\n\n const ops = { 'text:equal': ['string', 'equals'], 'text:notequal': ['string', 'notEquals'], 'text:contain': ['string', 'contains'], 'text:notcontain': ['string', 'notContains'],\n 'number:equal': ['number', 'equals'], 'number:greater': ['number', 'gt'], 'number:less': ['number', 'lt'], exist: ['string', 'exists'], notexist: ['string', 'notExists'],\n 'boolean:true': ['boolean', 'true'], 'boolean:false': ['boolean', 'false'] };\n function conditions(f, note) {\n if (!f) return { conds: [{ id: 'c0', leftValue: '={{ true }}', rightValue: '', operator: { type: 'boolean', operation: 'true', singleValue: true } }], combinator: 'and' };\n const multiOr = f.groups.length > 1 && f.groups.some((g) => g.length > 1);\n if (multiOr) note.push('filter mixes AND/OR groups: only the first group was converted');\n const flat = f.groups.length > 1 && !multiOr ? f.groups.map((g) => g[0]) : f.groups[0] ?? [];\n return {\n combinator: f.groups.length > 1 && !multiOr ? 'or' : 'and',\n conds: flat.map((c, i) => { const [type, operation] = ops[c.op] ?? ['string', 'equals']; if (!ops[c.op]) note.push(`unknown filter operator ${c.op}`);\n return { id: `c${i}`, leftValue: tx(c.a), rightValue: c.b ?? '', operator: { type, operation, ...(['exists', 'notExists', 'true', 'false'].includes(operation) ? { singleValue: true } : {}) } }; }),\n };\n }\n const ifParams = (c) => ({ conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'loose', version: 2 }, conditions: c.conds, combinator: c.combinator }, options: {} });\n const add = (name, type, version, pos, params, extra = {}) => { nodes.push({ parameters: params, id: hash(sc.name + name, 8) + '-' + nodes.length, name, type, typeVersion: version, position: pos, ...extra }); return name; };\n\n // Register names first so references to later/earlier steps resolve\n (function reg(steps) { for (const st of steps) { byKey[st.key] = uniq(st.label); viaBody[st.key] = st.kind === 'trigger' && (ALIAS[st.app] ?? st.app) === 'webhook'; if (st.routes) st.routes.forEach((r) => reg(r.steps)); } })(sc.steps);\n\n let row = 0;\n function build(steps, prev, prevOut, depth) {\n for (const st of steps) {\n const y = row * 200, note = [];\n if (st.filter) {\n const c = conditions(st.filter, note);\n const f = add(uniq(`Filter: ${st.filter.label}`.slice(0, 50)), 'n8n-nodes-base.if', 2.2, [depth * 240, y], ifParams(c));\n rows.push({ step: `filter before ${st.label}`, source: 'filter', n8n: 'If', status: note.length ? 'partial' : 'mapped', notes: note.splice(0) });\n if (prev) link(prev, f, prevOut); prev = f; prevOut = 0; depth++;\n }\n const name = byKey[st.key];\n flags.length = 0;\n if (st.kind === 'router') {\n const rules = st.routes.map((r, i) => { const f = r.filter ?? r.steps[0]?.filter; if (!f) note.push(`route \"${r.label}\" has no condition, so it always runs (n8n sends items to every matching output)`); const c = conditions(f, note); return { conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'loose', version: 2 }, conditions: c.conds, combinator: c.combinator }, renameOutput: true, outputKey: r.label.slice(0, 40) || `Route ${i + 1}` }; });\n add(name, 'n8n-nodes-base.switch', 3.2, [depth * 240, y], { rules: { values: rules }, options: { allMatchingOutputs: true } }); // Make routers and Zapier paths run every matching branch\n rows.push({ step: st.label, source: `${st.app_raw}`, n8n: 'Switch', status: note.some((n) => !n.startsWith('route \"')) ? 'partial' : 'mapped', notes: [`${st.routes.length} routes`, ...note] });\n if (prev) link(prev, name, prevOut);\n st.routes.forEach((r, i) => { if (i > 0) row++; r.steps.forEach((x) => { if (x === r.steps[0]) x.filter = null; }); build(r.steps, name, i, depth + 1); });\n return;\n }\n const entry = APPS[ALIAS[st.app] ?? st.app];\n let type, version, mapped = false, params = {}, cred = null;\n if (st.kind === 'iterator') { type = 'n8n-nodes-base.splitOut'; version = 1; const arr = String(st.params.array ?? ''); params = { fieldToSplitOut: arr.replace(/\\{\\{\\s*\\d+\\.([\\w.]+)\\s*\\}\\}/, '$1') || 'items', options: {} }; note.push('Split Out reads the field from the previous node; check it'); }\n else if (st.kind === 'aggregator') { type = 'n8n-nodes-base.aggregate'; version = 1; params = { aggregate: 'aggregateAllItemData', options: {} }; note.push('aggregates all items; Make aggregators often build text or arrays, check the output'); }\n else if (entry) {\n const t = st.kind === 'trigger' ? entry.trigger ?? null : entry.action ?? null;\n if (t) { [type, version] = t; [params, mapped] = paramsFor(type, st); cred = entry.credential ?? null; if (!mapped) note.push(`set the ${st.action || 'operation'} fields (original settings are in the node notes)`); }\n }\n if (!type) {\n type = st.kind === 'trigger' ? 'n8n-nodes-base.manualTrigger' : 'n8n-nodes-base.noOp'; version = 1;\n tx(st.params); // still collect untranslatable expressions for the report\n note.push(`${st.app_raw}${st.action ? ' / ' + st.action : ''} isn't in the mapping table: add it to app_map in Settings if n8n has a node for it, or rebuild with HTTP Request`);\n }\n note.push(...flags);\n const extra = { notes: `Migrated from ${st.app_raw} ${st.action}\\n${JSON.stringify(st.params).slice(0, 900)}`, notesInFlow: false };\n if (type === 'n8n-nodes-base.webhook') extra.webhookId = hash(sc.name + st.key, 12);\n add(name, type, version, [depth * 240, y], params, extra);\n const status = type === 'n8n-nodes-base.noOp' || type === 'n8n-nodes-base.manualTrigger' ? 'manual' : mapped && !flags.length ? 'mapped' : 'partial';\n rows.push({ step: st.label, source: `${st.app_raw} ${st.action}`.trim(), n8n: type.split('.').pop(), status, credential: cred, trigger: st.kind === 'trigger', notes: note });\n if (prev) link(prev, name, prevOut);\n prev = name; prevOut = 0; depth++;\n }\n }\n\n // Zapier/Make always start with a trigger; if the first step isn't one we can trigger on, add a schedule.\n let start = null;\n if (sc.steps[0]?.kind !== 'trigger') {\n start = add(uniq('Every 15 Minutes'), 'n8n-nodes-base.scheduleTrigger', 1.2, [-240, 0], { rule: { interval: [{ field: 'minutes', minutesInterval: 15 }] } });\n rows.push({ step: 'schedule', source: 'scenario schedule', n8n: 'scheduleTrigger', status: 'partial', trigger: true, notes: ['set the interval to match the old schedule'] });\n }\n build(sc.steps, start, 0, 0);\n\n const id = hash(`${sc.source}:${sc.name}`, 16);\n return {\n name: sc.name, source: sc.source, monthly_runs: sc.monthly_runs, steps: rows,\n workflow: { id, name: `Migrated: ${sc.name}`, nodes, connections, settings: { executionOrder: 'v1' }, active: false, pinData: {} },\n };\n}\n\nconst d = $input.first().json;\nif (!d.ok) return [{ json: d }];\nreturn [{ json: { ...d, converted: d.scenarios.map(convert) } }];\n"
},
"id": "m23-convert-to-n8n",
"name": "Convert to n8n",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
920,
0
]
},
{
"parameters": {
"jsCode": "// Migration report per scenario: coverage, credentials, effort and cost estimates, and a parallel-run checklist.\nconst s = $('Settings (edit me)').first().json;\nconst d = $input.first().json;\nif (!d.ok) return [{ json: { ok: false, error: d.error } }];\nconst money = (n) => '$' + Math.round(n).toLocaleString('en-US');\n\nconst scenarios = d.converted.map((c) => {\n const count = (st) => c.steps.filter((r) => r.status === st).length;\n const mapped = count('mapped'), partial = count('partial'), manual = count('manual');\n const hours = Math.round((mapped * s.hours_mapped + partial * s.hours_partial + manual * s.hours_manual + s.hours_testing) * 4) / 4;\n // Billing units: Zapier bills tasks (actions that run, not triggers/filters); Make bills operations (every module run).\n // Routers/paths and filters aren't billed; Zapier doesn't bill the trigger either.\n const units = c.steps.filter((r) => r.source !== 'filter' && r.n8n !== 'Switch' && !(c.source === 'zapier' && r.trigger)).length;\n const monthlyUnits = units * c.monthly_runs;\n const oldCost = c.source === 'zapier' ? (monthlyUnits / 1000) * s.zapier_cost_per_1000_tasks : (monthlyUnits / 10000) * s.make_cost_per_10000_ops;\n const credentials = [...new Set(c.steps.map((r) => r.credential).filter(Boolean))];\n const checklist = [\n ...credentials.map((cr) => `Create the ${cr} credential in n8n and select it on its nodes`),\n ...c.steps.filter((r) => r.status !== 'mapped').map((r) => `${r.status === 'manual' ? 'Rebuild' : 'Check'} \"${r.step}\": ${r.notes.join('; ')}`),\n 'Run both versions side by side for a week and compare outputs',\n 'Point webhooks/forms at n8n, then switch off the old scenario',\n 'Attach an error workflow (module 04) and watch executions for the first days',\n ];\n return {\n name: c.name, source: c.source, workflow_id: c.workflow.id,\n coverage: { steps: c.steps.length, mapped, partial, manual, percent_ready: c.steps.length ? Math.round((mapped / c.steps.length) * 100) : 0 },\n credentials, effort_hours: hours,\n cost: { monthly_runs: c.monthly_runs, billable_units_per_run: units, old_monthly_estimate: Math.round(oldCost * 100) / 100,\n note: `${c.source === 'zapier' ? 'Zapier tasks' : 'Make operations'} at the rates in Settings; n8n self-hosted is a flat server cost (${money(s.n8n_monthly_cost)}/month for all workflows)` },\n steps: c.steps, checklist, workflow: c.workflow,\n };\n});\n\nconst totalOld = scenarios.reduce((t, x) => t + x.cost.old_monthly_estimate, 0);\nconst totalHours = scenarios.reduce((t, x) => t + x.effort_hours, 0);\nconst md = [\n `# Migration report (${scenarios.length} scenario${scenarios.length === 1 ? '' : 's'})`, '',\n `Estimated rebuild effort: **${totalHours} h** (${money(totalHours * s.hourly_rate)} at ${money(s.hourly_rate)}/h). Current platform cost estimate: **${money(totalOld)}/month** vs n8n self-hosted ~${money(s.n8n_monthly_cost)}/month. Estimates only; check your actual plan.`, '',\n ...scenarios.flatMap((x) => [\n `## ${x.name} (${x.source})`, '',\n `${x.coverage.mapped}/${x.coverage.steps} steps ready, ${x.coverage.partial} to check, ${x.coverage.manual} to rebuild · ${x.effort_hours} h · credentials: ${x.credentials.join(', ') || 'none'}`, '',\n '| Step | From | n8n node | Status |', '|---|---|---|---|',\n ...x.steps.map((r) => `| ${r.step} | ${r.source} | ${r.n8n} | ${r.status}${r.notes.length ? ': ' + r.notes.join('; ') : ''} |`), '',\n '**Checklist**', ...x.checklist.map((c) => `- [ ] ${c}`), '',\n ]),\n].join('\\n');\n\nreturn [{ json: { ok: true, dry_run: d.dry_run, create: d.create && !d.dry_run, totals: { scenarios: scenarios.length, effort_hours: totalHours, old_monthly_estimate: Math.round(totalOld * 100) / 100, n8n_monthly_cost: s.n8n_monthly_cost }, scenarios, report_markdown: md } }];\n"
},
"id": "m23-build-report",
"name": "Build Report",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1140,
0
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.ok }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "m23-valid-export",
"name": "Valid Export?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1360,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: false, error: $json.error } }}",
"options": {
"responseCode": 400
}
},
"id": "m23-respond-error",
"name": "Respond Error",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1580,
180
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.create }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "m23-create-in-n8n",
"name": "Create in n8n?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1580,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "m23-respond-report",
"name": "Respond Report",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1800,
140
]
},
{
"parameters": {
"jsCode": "// One item per converted workflow, shaped for the n8n API (it rejects id, active and pinData).\nreturn $input.first().json.scenarios.map((x) => ({ json: { name: x.name, body: { name: x.workflow.name, nodes: x.workflow.nodes, connections: x.workflow.connections, settings: x.workflow.settings } } }));\n"
},
"id": "m23-workflows-to-create",
"name": "Workflows to Create",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1800,
-100
]
},
{
"parameters": {
"resource": "workflow",
"operation": "create",
"workflowObject": "={{ JSON.stringify($json.body) }}",
"requestOptions": {}
},
"id": "m23-create-workflow",
"name": "Create Workflow",
"type": "n8n-nodes-base.n8n",
"typeVersion": 1,
"position": [
2020,
-100
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// Attach the n8n API result to each scenario: created only if n8n returned a workflow id.\nconst rep = $('Build Report').first().json;\nconst results = $input.all().map((i) => i.json);\nconst scenarios = rep.scenarios.map((x, i) => {\n const r = results[i] ?? {};\n const ok = Boolean(r.id) && !r.error;\n return { ...x, created: ok, created_id: ok ? r.id : null, create_error: ok ? null : String(r.error?.message ?? r.error ?? r.message ?? 'no workflow id returned (check the n8n API credential)').slice(0, 300) };\n});\nreturn [{ json: { ...rep, scenarios } }];\n"
},
"id": "m23-record-created",
"name": "Record Created",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2240,
-100
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "m23-respond-created",
"name": "Respond Created",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
2460,
-100
]
},
{
"parameters": {
"jsCode": "// What the converter knows: apps with trigger/action equivalents and their aliases.\nconst s = $('Settings (edit me)').first().json;\nconst apps = Object.entries(s.app_map).map(([app, m]) => ({ app, trigger: m.trigger?.[0] ?? null, action: m.action?.[0] ?? null, credential: m.credential ?? null,\n aliases: Object.entries(s.app_aliases).filter(([, v]) => v === app).map(([k]) => k) }));\nreturn [{ json: { count: apps.length, apps } }];\n"
},
"id": "m23-list-mappings",
"name": "List Mappings",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
400
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {}
},
"id": "m23-respond-mappings",
"name": "Respond Mappings",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
920,
400
]
}
],
"connections": {
"Convert API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Mappings API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Settings (edit me)": {
"main": [
[
{
"node": "Route",
"type": "main",
"index": 0
}
]
]
},
"Route": {
"main": [
[
{
"node": "Read Export",
"type": "main",
"index": 0
}
],
[
{
"node": "List Mappings",
"type": "main",
"index": 0
}
]
]
},
"Read Export": {
"main": [
[
{
"node": "Convert to n8n",
"type": "main",
"index": 0
}
]
]
},
"Convert to n8n": {
"main": [
[
{
"node": "Build Report",
"type": "main",
"index": 0
}
]
]
},
"Build Report": {
"main": [
[
{
"node": "Valid Export?",
"type": "main",
"index": 0
}
]
]
},
"Valid Export?": {
"main": [
[
{
"node": "Create in n8n?",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Error",
"type": "main",
"index": 0
}
]
]
},
"Create in n8n?": {
"main": [
[
{
"node": "Workflows to Create",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Report",
"type": "main",
"index": 0
}
]
]
},
"Workflows to Create": {
"main": [
[
{
"node": "Create Workflow",
"type": "main",
"index": 0
}
]
]
},
"Create Workflow": {
"main": [
[
{
"node": "Record Created",
"type": "main",
"index": 0
}
]
]
},
"Record Created": {
"main": [
[
{
"node": "Respond Created",
"type": "main",
"index": 0
}
]
]
},
"List Mappings": {
"main": [
[
{
"node": "Respond Mappings",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"pinData": {}
}