Zapier/Make to n8n Migration Kit
Module 23 · ship · advanced
report.js
// Migration report per scenario: coverage, credentials, effort and cost estimates, and a parallel-run checklist.
const s = $('Settings (edit me)').first().json;
const d = $input.first().json;
if (!d.ok) return [{ json: { ok: false, error: d.error } }];
const money = (n) => '$' + Math.round(n).toLocaleString('en-US');
const scenarios = d.converted.map((c) => {
const count = (st) => c.steps.filter((r) => r.status === st).length;
const mapped = count('mapped'), partial = count('partial'), manual = count('manual');
const hours = Math.round((mapped * s.hours_mapped + partial * s.hours_partial + manual * s.hours_manual + s.hours_testing) * 4) / 4;
// Billing units: Zapier bills tasks (actions that run, not triggers/filters); Make bills operations (every module run).
// Routers/paths and filters aren't billed; Zapier doesn't bill the trigger either.
const units = c.steps.filter((r) => r.source !== 'filter' && r.n8n !== 'Switch' && !(c.source === 'zapier' && r.trigger)).length;
const monthlyUnits = units * c.monthly_runs;
const oldCost = c.source === 'zapier' ? (monthlyUnits / 1000) * s.zapier_cost_per_1000_tasks : (monthlyUnits / 10000) * s.make_cost_per_10000_ops;
const credentials = [...new Set(c.steps.map((r) => r.credential).filter(Boolean))];
const checklist = [
...credentials.map((cr) => `Create the ${cr} credential in n8n and select it on its nodes`),
...c.steps.filter((r) => r.status !== 'mapped').map((r) => `${r.status === 'manual' ? 'Rebuild' : 'Check'} "${r.step}": ${r.notes.join('; ')}`),
'Run both versions side by side for a week and compare outputs',
'Point webhooks/forms at n8n, then switch off the old scenario',
'Attach an error workflow (module 04) and watch executions for the first days',
];
return {
name: c.name, source: c.source, workflow_id: c.workflow.id,
coverage: { steps: c.steps.length, mapped, partial, manual, percent_ready: c.steps.length ? Math.round((mapped / c.steps.length) * 100) : 0 },
credentials, effort_hours: hours,
cost: { monthly_runs: c.monthly_runs, billable_units_per_run: units, old_monthly_estimate: Math.round(oldCost * 100) / 100,
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)` },
steps: c.steps, checklist, workflow: c.workflow,
};
});
const totalOld = scenarios.reduce((t, x) => t + x.cost.old_monthly_estimate, 0);
const totalHours = scenarios.reduce((t, x) => t + x.effort_hours, 0);
const md = [
`# Migration report (${scenarios.length} scenario${scenarios.length === 1 ? '' : 's'})`, '',
`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.`, '',
...scenarios.flatMap((x) => [
`## ${x.name} (${x.source})`, '',
`${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'}`, '',
'| Step | From | n8n node | Status |', '|---|---|---|---|',
...x.steps.map((r) => `| ${r.step} | ${r.source} | ${r.n8n} | ${r.status}${r.notes.length ? ': ' + r.notes.join('; ') : ''} |`), '',
'**Checklist**', ...x.checklist.map((c) => `- [ ] ${c}`), '',
]),
].join('\n');
return [{ 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 } }];