Skip to content
// Turn each scenario's step tree into an n8n workflow (nodes, connections, layout) and record how well
// every step mapped: "mapped" (ready), "partial" (right node, check settings) or "manual" (no equivalent).
const s = $('Settings (edit me)').first().json;
const APPS = s.app_map;
const ALIAS = s.app_aliases;

// Small deterministic hash (no crypto in the Code sandbox) for stable workflow/webhook ids
const 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); }
  return ((h1 >>> 0).toString(36) + (h2 >>> 0).toString(36) + '0000000000000000').slice(0, len); };
const slug = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'hook';

function convert(sc) {
  const nodes = [], connections = {}, rows = [], used = new Set(), byKey = {}, viaBody = {};
  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; };
  const link = (from, to, out = 0) => {
    const c = (connections[from] ??= { main: [] }).main; while (c.length <= out) c.push([]);
    c[out].push({ node: to, type: 'main', index: 0 });
  };

  // Rewrite Make {{3.email}} / Zapier {{123__email}} references to n8n expressions.
  const flags = [];
  function tx(v) {
    if (Array.isArray(v)) return v.map(tx);
    if (v && typeof v === 'object') return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, tx(x)]));
    if (typeof v !== 'string' || !v.includes('{{')) return v;
    const ref = (key, path) => { const name = byKey[key]; if (!name) { flags.push(`unknown step reference ${key}`); return null; }
      if (viaBody[key]) path = 'body.' + path; // n8n's Webhook node puts the request body under .body
      return `$('${name}').item.json${path.split(/[.]/).map((p) => (/^[A-Za-z_]\w*$/.test(p) ? '.' + p : `['${p}']`)).join('')}`; };
    let out = v
      .replace(/\{\{\s*(\d+)\.([\w.]+)\s*\}\}/g, (m, k, p) => { const r = ref(k, p); return r ? `{{ ${r} }}` : m; })             // Make
      .replace(/\{\{\s*(\d+)__([\w]+?)\s*\}\}/g, (m, k, p) => { const r = ref(k, p.replace(/__/g, '.')); return r ? `{{ ${r} }}` : m; }); // Zapier
    const leftover = out.match(/\{\{(?! \$\(')[^}]*\}\}/g);
    if (leftover) flags.push(`translate by hand: ${leftover.slice(0, 2).join(' ')}`);
    return '=' + out;
  }

  // n8n parameters for the node types we know how to fill in. Returns [params, fullyMapped].
  function paramsFor(type, st) {
    const p = tx(st.params);
    const pick = (...ks) => { for (const k of ks) if (p[k] !== undefined && p[k] !== '') return p[k]; return undefined; };
    const list = (x) => (Array.isArray(x) ? x.map((e) => (typeof e === 'object' ? e.address ?? e.email ?? JSON.stringify(e) : e)).join(',') : x);
    switch (type) {
      case 'n8n-nodes-base.webhook': return [{ httpMethod: 'POST', path: `migrated/${slug(st.label)}`, options: {} }, true];
      case 'n8n-nodes-base.scheduleTrigger': return [{ rule: { interval: [{ field: 'minutes', minutesInterval: Number(st.params.interval ?? 15) }] } }, true];
      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');
        return [{ sendTo: to ?? '', subject: subject ?? '', emailType: 'text', message: message ?? '', options: {} }, Boolean(to && subject && message)]; }
      case 'n8n-nodes-base.slack': { const channel = pick('channel', 'channelId'); const text = pick('text', 'message');
        return [{ resource: 'message', operation: 'post', select: 'channel', channelId: { __rl: true, mode: 'name', value: String(channel ?? '').replace(/^=/, '') }, text: text ?? '', otherOptions: {} }, Boolean(channel && text)]; }
      case 'n8n-nodes-base.httpRequest': { const url = pick('url', 'URL'); const method = String(pick('method') ?? 'GET').replace(/^=/, '').toUpperCase();
        return [{ method, url: url ?? '', options: {} }, Boolean(url) && !pick('data', 'body')]; }
      case 'n8n-nodes-base.googleSheets': { const doc = pick('spreadsheetId', 'spreadsheet'); const sheet = pick('sheetId', 'sheet', 'worksheet');
        return [{ operation: 'append', documentId: { __rl: true, mode: 'id', value: String(doc ?? '').replace(/^=/, '') }, sheetName: { __rl: true, mode: 'name', value: String(sheet ?? '').replace(/^=/, '') },
          columns: { mappingMode: 'defineBelow', value: p.values ?? p.row ?? {} }, options: {} }, false]; } // column mapping always needs a look
      case 'n8n-nodes-base.set': { const vars = p.variables ?? (p.name ? [{ name: p.name, value: p.value }] : []);
        return [{ assignments: { assignments: vars.map((x, i) => ({ id: `a${i}`, name: String(x.name).replace(/^=/, ''), value: x.value, type: 'string' })) }, options: {} }, vars.length > 0]; }
      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)];
      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];
      default: return [{}, false];
    }
  }

  const ops = { 'text:equal': ['string', 'equals'], 'text:notequal': ['string', 'notEquals'], 'text:contain': ['string', 'contains'], 'text:notcontain': ['string', 'notContains'],
    'number:equal': ['number', 'equals'], 'number:greater': ['number', 'gt'], 'number:less': ['number', 'lt'], exist: ['string', 'exists'], notexist: ['string', 'notExists'],
    'boolean:true': ['boolean', 'true'], 'boolean:false': ['boolean', 'false'] };
  function conditions(f, note) {
    if (!f) return { conds: [{ id: 'c0', leftValue: '={{ true }}', rightValue: '', operator: { type: 'boolean', operation: 'true', singleValue: true } }], combinator: 'and' };
    const multiOr = f.groups.length > 1 && f.groups.some((g) => g.length > 1);
    if (multiOr) note.push('filter mixes AND/OR groups: only the first group was converted');
    const flat = f.groups.length > 1 && !multiOr ? f.groups.map((g) => g[0]) : f.groups[0] ?? [];
    return {
      combinator: f.groups.length > 1 && !multiOr ? 'or' : 'and',
      conds: flat.map((c, i) => { const [type, operation] = ops[c.op] ?? ['string', 'equals']; if (!ops[c.op]) note.push(`unknown filter operator ${c.op}`);
        return { id: `c${i}`, leftValue: tx(c.a), rightValue: c.b ?? '', operator: { type, operation, ...(['exists', 'notExists', 'true', 'false'].includes(operation) ? { singleValue: true } : {}) } }; }),
    };
  }
  const ifParams = (c) => ({ conditions: { options: { caseSensitive: true, leftValue: '', typeValidation: 'loose', version: 2 }, conditions: c.conds, combinator: c.combinator }, options: {} });
  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; };

  // Register names first so references to later/earlier steps resolve
  (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);

  let row = 0;
  function build(steps, prev, prevOut, depth) {
    for (const st of steps) {
      const y = row * 200, note = [];
      if (st.filter) {
        const c = conditions(st.filter, note);
        const f = add(uniq(`Filter: ${st.filter.label}`.slice(0, 50)), 'n8n-nodes-base.if', 2.2, [depth * 240, y], ifParams(c));
        rows.push({ step: `filter before ${st.label}`, source: 'filter', n8n: 'If', status: note.length ? 'partial' : 'mapped', notes: note.splice(0) });
        if (prev) link(prev, f, prevOut); prev = f; prevOut = 0; depth++;
      }
      const name = byKey[st.key];
      flags.length = 0;
      if (st.kind === 'router') {
        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}` }; });
        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
        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] });
        if (prev) link(prev, name, prevOut);
        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); });
        return;
      }
      const entry = APPS[ALIAS[st.app] ?? st.app];
      let type, version, mapped = false, params = {}, cred = null;
      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'); }
      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'); }
      else if (entry) {
        const t = st.kind === 'trigger' ? entry.trigger ?? null : entry.action ?? null;
        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)`); }
      }
      if (!type) {
        type = st.kind === 'trigger' ? 'n8n-nodes-base.manualTrigger' : 'n8n-nodes-base.noOp'; version = 1;
        tx(st.params); // still collect untranslatable expressions for the report
        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`);
      }
      note.push(...flags);
      const extra = { notes: `Migrated from ${st.app_raw} ${st.action}\n${JSON.stringify(st.params).slice(0, 900)}`, notesInFlow: false };
      if (type === 'n8n-nodes-base.webhook') extra.webhookId = hash(sc.name + st.key, 12);
      add(name, type, version, [depth * 240, y], params, extra);
      const status = type === 'n8n-nodes-base.noOp' || type === 'n8n-nodes-base.manualTrigger' ? 'manual' : mapped && !flags.length ? 'mapped' : 'partial';
      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 });
      if (prev) link(prev, name, prevOut);
      prev = name; prevOut = 0; depth++;
    }
  }

  // Zapier/Make always start with a trigger; if the first step isn't one we can trigger on, add a schedule.
  let start = null;
  if (sc.steps[0]?.kind !== 'trigger') {
    start = add(uniq('Every 15 Minutes'), 'n8n-nodes-base.scheduleTrigger', 1.2, [-240, 0], { rule: { interval: [{ field: 'minutes', minutesInterval: 15 }] } });
    rows.push({ step: 'schedule', source: 'scenario schedule', n8n: 'scheduleTrigger', status: 'partial', trigger: true, notes: ['set the interval to match the old schedule'] });
  }
  build(sc.steps, start, 0, 0);

  const id = hash(`${sc.source}:${sc.name}`, 16);
  return {
    name: sc.name, source: sc.source, monthly_runs: sc.monthly_runs, steps: rows,
    workflow: { id, name: `Migrated: ${sc.name}`, nodes, connections, settings: { executionOrder: 'v1' }, active: false, pinData: {} },
  };
}

const d = $input.first().json;
if (!d.ok) return [{ json: d }];
return [{ json: { ...d, converted: d.scenarios.map(convert) } }];