[
  {
    "updatedAt": "2026-10-26T16:09:57.037Z",
    "createdAt": "2026-10-23T07:20:59.705Z",
    "id": "ScrapeViewsApify",
    "name": "ImpactView — Get All Views (Apify)",
    "description": null,
    "active": false,
    "isArchived": false,
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "scrape-views",
          "responseMode": "responseNode",
          "authentication": "headerAuth",
          "options": {}
        },
        "id": "webhook-scrape",
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          -680,
          0
        ],
        "webhookId": "ee000000-0000-4000-8000-0000000000f2",
        "credentials": {
          "httpHeaderAuth": {
            "id": "nvkabokeng6itHSl",
            "name": "ImpactView n8n Webhook"
          }
        },
        "notesInFlow": true,
        "notes": "POST /scrape-views — body {event_id}"
      },
      {
        "parameters": {
          "url": "=https://impactview.mdnetwork.web.id/api/v1/n8n/events/{{ $('Webhook').first().json.body.event_id }}/performance",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth",
          "options": {
            "timeout": 60000
          }
        },
        "id": "performa",
        "name": "Ambil Performa Event",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -440,
          0
        ],
        "credentials": {
          "httpHeaderAuth": {
            "id": "6jYfhBUaEAyf7Q6l",
            "name": "ImpactView API"
          }
        },
        "retryOnFail": true,
        "maxTries": 2,
        "waitBetweenTries": 2000
      },
      {
        "parameters": {
          "jsCode": "\n// Pisahkan deliverable per platform dan catat info untuk menyusun log di UI.\n// URL dikirim unik ke Apify supaya kredit tidak terbuang oleh duplikat.\nconst payload = $input.first().json;\n\nconst reIg = /(?:instagram\\.com|instagr\\.am)\\/(?:reel|reels|p|tv)\\/([A-Za-z0-9_-]+)/i;\nconst reTk = /tiktok\\.com\\/[^\\/]*\\/video\\/(\\d+)/i;\nconst reTkShort = /vt\\.tiktok\\.com\\//i;\nconst reFb = /(?:facebook\\.com|fb\\.watch|fb\\.me)/i;\n\nconst instagram = [];\nconst tiktok = [];\nconst lainnya = [];\nconst dilewati = [];\n\nfor (const d of (payload.deliverables || [])) {\n  const url = d.tracking_url || '';\n\n  if (!url) {\n    dilewati.push({ deliverable_id: d.id, alasan: 'live_post_url kosong' });\n    continue;\n  }\n\n  let m;\n  if ((m = url.match(reIg))) {\n    instagram.push({ deliverable_id: d.id, shortcode: m[1], url });\n  } else if ((m = url.match(reTk))) {\n    tiktok.push({ deliverable_id: d.id, video_id: m[1], url });\n  } else if (reTkShort.test(url)) {\n    // Link pendek tidak memuat video id sehingga hasil scrape tidak bisa\n    // dipetakan balik ke deliverable-nya.\n    lainnya.push({ deliverable_id: d.id, url, platform: 'tiktok', alasan: 'link pendek (vt.tiktok.com)' });\n  } else if (reFb.test(url)) {\n    lainnya.push({ deliverable_id: d.id, url, platform: 'facebook', alasan: 'belum ada actor Facebook' });\n  } else {\n    dilewati.push({ deliverable_id: d.id, url, alasan: 'platform tidak dikenali' });\n  }\n}\n\n// Info tampilan untuk log.\nconst info = {};\nfor (const d of (payload.deliverables || [])) {\n  info[d.id] = {\n    kol_name: d.kol_name || 'KOL',\n    title: d.content_format ? `${d.kol_name || 'KOL'} · ${d.content_format}` : (d.kol_name || 'KOL'),\n    url: d.tracking_url || '',\n  };\n}\n\nreturn [{\n  json: {\n    event_id: payload.event?.id ?? null,\n    instagram,\n    tiktok,\n    lainnya,\n    dilewati,\n    info,\n    ig_urls: [...new Set(instagram.map((x) => x.url))],\n    tiktok_urls: [...new Set(tiktok.map((x) => x.url))],\n  },\n}];\n",
          "mode": "runOnceForAllItems"
        },
        "id": "ekstrak",
        "name": "Ekstrak Link & Shortcode",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -200,
          0
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://api.apify.com/v2/acts/patient_discovery~instagram-reel-analytics-by-url/run-sync-get-dataset-items",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpQueryAuth",
          "sendQuery": true,
          "specifyQuery": "keypair",
          "queryParameters": {
            "parameters": [
              {
                "name": "memory",
                "value": "2048"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ postUrls: $json.ig_urls }) }}",
          "options": {
            "timeout": 300000
          }
        },
        "id": "apify-ig",
        "name": "Apify — Instagram",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          40,
          0
        ],
        "credentials": {
          "httpQueryAuth": {
            "id": "NPdRlHHRhKiecZi5",
            "name": "Apify Token"
          }
        },
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 15000,
        "onError": "continueRegularOutput",
        "notesInFlow": true,
        "notes": "memory=2048 · gagal tidak mematikan workflow"
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://api.apify.com/v2/acts/clockworks~tiktok-scraper/run-sync-get-dataset-items",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpQueryAuth",
          "sendQuery": true,
          "specifyQuery": "keypair",
          "queryParameters": {
            "parameters": [
              {
                "name": "memory",
                "value": "2048"
              }
            ]
          },
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ postURLs: $('Ekstrak Link & Shortcode').first().json.tiktok_urls }) }}",
          "options": {
            "timeout": 300000
          }
        },
        "id": "apify-tk",
        "name": "Apify — TikTok",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          280,
          0
        ],
        "credentials": {
          "httpQueryAuth": {
            "id": "NPdRlHHRhKiecZi5",
            "name": "Apify Token"
          }
        },
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 15000,
        "onError": "continueRegularOutput",
        "notesInFlow": true,
        "notes": "clockworks~tiktok-scraper · memory=2048 · gagal tidak mematikan workflow"
      },
      {
        "parameters": {
          "jsCode": "\n// Petakan hasil Apify balik ke deliverable_id.\n// Satu URL bisa dipakai lebih dari satu deliverable (mis. dua task menunjuk\n// postingan yang sama), jadi peta menyimpan daftar id.\nconst ext = $('Ekstrak Link & Shortcode').first().json;\n\nconst petaIg = {};\nfor (const x of ext.instagram) {\n  if (!petaIg[x.shortcode]) petaIg[x.shortcode] = [];\n  petaIg[x.shortcode].push(x.deliverable_id);\n}\n\nconst petaTk = {};\nfor (const x of ext.tiktok) {\n  if (!x.video_id) continue;\n  if (!petaTk[x.video_id]) petaTk[x.video_id] = [];\n  petaTk[x.video_id].push(x.deliverable_id);\n}\n\nconst sync_items = [];\nconst tak_cocok = [];\nconst tanpa_views = [];\nconst sudah = new Set();\n\nfunction tambah(ids, views, likes, comments, shares, saves, platform) {\n  for (const id of ids) {\n    if (sudah.has(id)) continue;\n    sudah.add(id);\n\n    const item = {\n      deliverable_id: id,\n      views: Number(views) || 0,\n      likes: Number(likes) || 0,\n      comments: Number(comments) || 0,\n      platform,\n    };\n    // Hanya sertakan bila benar-benar ada — mengirim 0 akan menimpa data asli.\n    if (shares != null) item.shares = Number(shares) || 0;\n    if (saves != null) item.saves = Number(saves) || 0;\n\n    sync_items.push(item);\n  }\n}\n\nlet hasilIg = [];\nlet hasilTk = [];\ntry { hasilIg = $('Apify — Instagram').all().map((i) => i.json); } catch (e) { hasilIg = []; }\ntry { hasilTk = $('Apify — TikTok').all().map((i) => i.json); } catch (e) { hasilTk = []; }\n\n// --- Instagram\nfor (const post of hasilIg) {\n  const code = post?.code;\n  if (!code) continue;                       // item error dari actor, mis. daftar kosong\n  const ids = petaIg[code];\n  if (!ids) { tak_cocok.push({ platform: 'instagram', code }); continue; }\n\n  const m = post.metrics || {};\n  const views = m.play_count ?? m.view_count;\n  if (views == null) { tanpa_views.push({ platform: 'instagram', code }); continue; }\n\n  tambah(ids, views, m.like_count, m.comment_count, m.share_count, m.save_count, 'instagram');\n}\n\n// --- TikTok (statistik ada di root item, bukan di dalam `metrics`)\nfor (const post of hasilTk) {\n  const vid = post?.id;\n  if (!vid) continue;\n  const ids = petaTk[vid];\n  if (!ids) { tak_cocok.push({ platform: 'tiktok', code: vid, error: post.error || null }); continue; }\n\n  const views = post.playCount ?? post.viewCount;\n  if (views == null) { tanpa_views.push({ platform: 'tiktok', code: vid }); continue; }\n\n  tambah(ids, views, post.diggCount, post.commentCount, post.shareCount, post.collectCount, 'tiktok');\n}\n\n// Actor yang gagal (mis. kuota Apify habis) tidak mengembalikan data asli.\n// Karena node memakai `onError: continueRegularOutput`, input-nya diteruskan\n// apa adanya — jadi jumlah item TIDAK bisa dipakai sebagai penanda sukses.\n// Yang benar: cek apakah ada item dengan penanda khas actor (code / id).\nconst adaDataIg = hasilIg.some((x) => x && x.code);\nconst adaDataTk = hasilTk.some((x) => x && x.id);\n\nconst masalah_apify = [];\nif (ext.instagram.length > 0 && !adaDataIg) {\n  masalah_apify.push('Actor Instagram tidak mengembalikan data (' + ext.instagram.length + ' URL dikirim) — cek kuota/limit Apify');\n}\nif (ext.tiktok.length > 0 && !adaDataTk) {\n  masalah_apify.push('Actor TikTok tidak mengembalikan data (' + ext.tiktok.length + ' URL dikirim) — cek kuota/limit Apify');\n}\n\n// Deliverable yang URL-nya dikirim ke Apify tapi tidak menghasilkan apa pun,\n// supaya tetap muncul di log dan tidak menghilang diam-diam.\nconst terjawab = new Set(sync_items.map((x) => x.deliverable_id));\nconst belum_terjawab = [...ext.instagram, ...ext.tiktok]\n  .filter((x) => !terjawab.has(x.deliverable_id))\n  .map((x) => ({ deliverable_id: x.deliverable_id, platform: x.platform || 'instagram' }));\n\nreturn [{\n  json: {\n    event_id: ext.event_id,\n    sync_items,\n    info: ext.info,\n    masalah_apify,\n    belum_terjawab,\n    lainnya: ext.lainnya,\n    dilewati: ext.dilewati,\n    catatan: {\n      url_instagram: ext.instagram.length,\n      url_tiktok: ext.tiktok.length,\n      jumlah_discrape: sync_items.length,\n      tak_cocok,\n      tanpa_views,\n    },\n  },\n}];\n",
          "mode": "runOnceForAllItems"
        },
        "id": "petakan",
        "name": "Petakan Hasil Scrape",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          520,
          0
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "=https://impactview.mdnetwork.web.id/api/v1/n8n/events/{{ $json.event_id }}/sync-scraped-metrics",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ scraped_items: $json.sync_items }) }}",
          "options": {
            "timeout": 120000
          }
        },
        "id": "simpan",
        "name": "Simpan Metrik ke ImpactView",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          760,
          0
        ],
        "credentials": {
          "httpHeaderAuth": {
            "id": "6jYfhBUaEAyf7Q6l",
            "name": "ImpactView API"
          }
        },
        "retryOnFail": true,
        "maxTries": 2,
        "waitBetweenTries": 2000,
        "onError": "continueRegularOutput"
      },
      {
        "parameters": {
          "jsCode": "\n// Bentuk keluaran disamakan dengan endpoint lama supaya modal di dashboard\n// tidak perlu diubah.\nconst d = $('Petakan Hasil Scrape').first().json;\nconst info = d.info || {};\n\nconst logs = [];\n\nfor (const s of d.sync_items) {\n  const meta = info[s.deliverable_id] || {};\n  logs.push({\n    deliverable_id: s.deliverable_id,\n    kol_name: meta.kol_name || 'KOL',\n    title: meta.title || 'Konten',\n    url: meta.url || '',\n    status: 'success',\n    views: s.views,\n    platform: s.platform,\n    sumber: 'apify-' + s.platform,\n    message: 'Berhasil: ' + Number(s.views).toLocaleString('id-ID') + ' views',\n  });\n}\n\nconst gagalIds = new Set([\n  ...(d.catatan?.tak_cocok || []).map((x) => x.code),\n  ...(d.catatan?.tanpa_views || []).map((x) => x.code),\n]);\n\nfor (const b of (d.belum_terjawab || [])) {\n  const meta = info[b.deliverable_id] || {};\n  logs.push({\n    deliverable_id: b.deliverable_id,\n    kol_name: meta.kol_name || 'KOL',\n    title: meta.title || 'Konten',\n    url: meta.url || '',\n    status: 'failed',\n    views: null,\n    platform: b.platform,\n    sumber: 'apify-' + b.platform,\n    message: 'Tidak ada hasil dari Apify',\n  });\n}\n\nfor (const l of (d.lainnya || [])) {\n  const meta = info[l.deliverable_id] || {};\n  logs.push({\n    deliverable_id: l.deliverable_id,\n    kol_name: meta.kol_name || 'KOL',\n    title: meta.title || 'Konten',\n    url: l.url,\n    status: 'dilewati',\n    views: null,\n    platform: l.platform,\n    sumber: 'n8n',\n    message: 'Dilewati: ' + (l.alasan || 'tidak ditangani'),\n  });\n}\n\nfor (const w of (d.dilewati || [])) {\n  const meta = info[w.deliverable_id] || {};\n  logs.push({\n    deliverable_id: w.deliverable_id,\n    kol_name: meta.kol_name || 'KOL',\n    title: meta.title || 'Konten',\n    url: w.url || '',\n    status: 'failed',\n    views: null,\n    sumber: 'n8n',\n    message: 'Dilewati: ' + w.alasan,\n  });\n}\n\nconst hitung = (s) => logs.filter((x) => x.status === s).length;\nconst ig = logs.filter((x) => x.sumber === 'apify-instagram').length;\nconst tk = logs.filter((x) => x.sumber === 'apify-tiktok').length;\n\nreturn [{\n  json: {\n    success: true,\n    sumber: 'n8n-apify',\n    summary: {\n      total: logs.length,\n      success: hitung('success'),\n      failed: hitung('failed'),\n      dilewati: hitung('dilewati'),\n      instagram: ig,\n      tiktok: tk,\n    },\n    catatan: d.catatan || {},\n    masalah_apify: d.masalah_apify || [],\n    logs,\n    message: `Selesai lewat Apify: ${hitung('success')} berhasil `\n      + `(${ig} Instagram, ${tk} TikTok), ${hitung('failed')} gagal, `\n      + `${hitung('dilewati')} dilewati.`\n      + ((d.masalah_apify || []).length ? ' PERHATIAN: ' + d.masalah_apify.join('; ') : ''),\n  },\n}];\n",
          "mode": "runOnceForAllItems"
        },
        "id": "log",
        "name": "Susun Log",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1000,
          0
        ]
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify($json) }}",
          "options": {
            "responseCode": 200
          }
        },
        "id": "respond",
        "name": "Respond to Webhook",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          1240,
          0
        ]
      }
    ],
    "connections": {
      "Webhook": {
        "main": [
          [
            {
              "node": "Ambil Performa Event",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Ambil Performa Event": {
        "main": [
          [
            {
              "node": "Ekstrak Link & Shortcode",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Ekstrak Link & Shortcode": {
        "main": [
          [
            {
              "node": "Apify — Instagram",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Apify — Instagram": {
        "main": [
          [
            {
              "node": "Apify — TikTok",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Apify — TikTok": {
        "main": [
          [
            {
              "node": "Petakan Hasil Scrape",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Petakan Hasil Scrape": {
        "main": [
          [
            {
              "node": "Simpan Metrik ke ImpactView",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Simpan Metrik ke ImpactView": {
        "main": [
          [
            {
              "node": "Susun Log",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Susun Log": {
        "main": [
          [
            {
              "node": "Respond to Webhook",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "settings": {
      "executionOrder": "v1",
      "saveManualExecutions": true,
      "callerPolicy": "workflowsFromSameOwner",
      "timezone": "Asia/Jakarta"
    },
    "staticData": null,
    "meta": null,
    "nodeGroups": [],
    "pinData": {},
    "versionId": "4351c85a-d147-43c8-a177-a74e5efb694e",
    "activeVersionId": null,
    "versionCounter": 5,
    "triggerCount": 1,
    "sourceWorkflowId": null,
    "tags": [],
    "shared": [
      {
        "updatedAt": "2026-10-23T07:20:59.705Z",
        "createdAt": "2026-10-23T07:20:59.705Z",
        "role": "workflow:owner",
        "workflowId": "ScrapeViewsApify",
        "projectId": "O5dS2R3vFvvVOZZK",
        "project": {
          "updatedAt": "2026-10-23T06:20:42.590Z",
          "createdAt": "2026-10-21T04:53:49.401Z",
          "id": "O5dS2R3vFvvVOZZK",
          "name": "Mohammad Khoiruman <arifkhoiruman2@gmail.com>",
          "type": "personal",
          "icon": null,
          "description": null,
          "customTelemetryTags": [],
          "creatorId": "77323fa2-5a74-405e-999c-961acc6ca1ba"
        }
      }
    ],
    "versionMetadata": {
      "name": null,
      "description": null
    }
  },
  {
    "updatedAt": "2026-10-26T16:10:08.194Z",
    "createdAt": "2026-10-21T22:10:17.223Z",
    "id": "HVucE5zuj9mR8H6k",
    "name": "ImpactView — Deep Intelligence & Apify Scraping (v2.1 diperbaiki)",
    "description": null,
    "active": false,
    "isArchived": false,
    "nodes": [
      {
        "parameters": {
          "rule": {
            "interval": [
              {
                "field": "cronExpression",
                "expression": "0 0 * * *"
              }
            ]
          }
        },
        "id": "cron-trigger",
        "name": "Cron (tengah malam WIB)",
        "type": "n8n-nodes-base.scheduleTrigger",
        "typeVersion": 1.2,
        "position": [
          -780,
          60
        ]
      },
      {
        "parameters": {},
        "id": "manual-trigger",
        "name": "Manual Trigger (jalankan sekarang)",
        "type": "n8n-nodes-base.manualTrigger",
        "typeVersion": 1,
        "position": [
          -780,
          -140
        ]
      },
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "trigger-event-analysis",
          "responseMode": "onReceived",
          "authentication": "headerAuth",
          "options": {}
        },
        "id": "webhook-trigger",
        "name": "Webhook (dari Dashboard)",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          -780,
          420
        ],
        "webhookId": "dd000000-0000-4000-8000-0000000000f1",
        "credentials": {
          "httpHeaderAuth": {
            "id": "nvkabokeng6itHSl",
            "name": "ImpactView n8n Webhook"
          }
        }
      },
      {
        "parameters": {
          "url": "https://impactview.mdnetwork.web.id/api/v1/n8n/events",
          "options": {
            "timeout": 30000
          },
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth"
        },
        "id": "get-active-events",
        "name": "Ambil Daftar Event Aktif",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -560,
          180
        ],
        "credentials": {
          "httpHeaderAuth": {
            "id": "6jYfhBUaEAyf7Q6l",
            "name": "ImpactView API"
          }
        },
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000
      },
      {
        "parameters": {
          "jsCode": "// Menyatukan dua sumber: Cron (daftar event) dan Webhook (satu event_id).\n// Keluaran: satu item per event, masing-masing berisi event_id.\nconst out = [];\n\nfor (const item of $input.all()) {\n  const j = item.json || {};\n\n  // PENTING: node Webhook n8n membungkus payload di dalam `body`.\n  // Cron / HTTP Request menaruh data langsung di root.\n  const payload = (j.body && typeof j.body === 'object') ? j.body : j;\n\n  // Dari Webhook: { event_id, event_name, triggered_at }\n  if (payload.event_id) {\n    out.push({ event_id: Number(payload.event_id), sumber: 'webhook' });\n    continue;\n  }\n\n  // Dari API daftar event: { count, events: [...] }\n  const list = Array.isArray(payload) ? payload : (payload.events || payload.data || []);\n  for (const e of list) {\n    if (e && e.id) out.push({ event_id: Number(e.id), nama: e.name, sumber: 'cron' });\n  }\n}\n\nif (out.length === 0) {\n  throw new Error('Tidak ada event untuk diproses. Cek endpoint /api/v1/n8n/events.');\n}\n\nreturn out.map(x => ({ json: x }));"
        },
        "id": "siapkan-event",
        "name": "Siapkan Event",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -340,
          300
        ]
      },
      {
        "parameters": {
          "options": {
            "timeout": 60000
          },
          "url": "=https://impactview.mdnetwork.web.id/api/v1/n8n/events/{{ $json.event_id }}/performance",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth"
        },
        "id": "ambil-performa-event",
        "name": "Ambil Performa Event",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -120,
          300
        ],
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000,
        "credentials": {
          "httpHeaderAuth": {
            "id": "6jYfhBUaEAyf7Q6l",
            "name": "ImpactView API"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Ambil shortcode Instagram dari tracking_url supaya hasil scrape bisa\n// dipetakan BALIK ke deliverable_id yang benar.\nconst payload = $input.first().json;\n\nconst reIg = /(?:instagram\\.com|instagr\\.am)\\/(?:reel|reels|p|tv)\\/([A-Za-z0-9_-]+)/i;\nconst reTk = /tiktok\\.com/i;\n\nconst instagram = [];\nconst tiktok = [];\nconst dilewati = [];\n\nfor (const d of (payload.deliverables || [])) {\n  const url = d.tracking_url || '';\n  if (!url) { dilewati.push({ deliverable_id: d.id, alasan: 'tracking_url kosong' }); continue; }\n\n  const m = url.match(reIg);\n  if (m) {\n    instagram.push({ deliverable_id: d.id, shortcode: m[1], url, platform: d.platform || 'instagram' });\n  } else if (reTk.test(url)) {\n    tiktok.push({ deliverable_id: d.id, url });\n  } else {\n    dilewati.push({ deliverable_id: d.id, url, alasan: 'platform tidak dikenali' });\n  }\n}\n\nreturn [{\n  json: {\n    event_id: payload.event?.id ?? null,\n    payload,\n    instagram,\n    ig_urls: instagram.map(x => x.url),\n    tiktok,\n    dilewati,\n  },\n}];"
        },
        "id": "ekstrak-link-&-shortcode",
        "name": "Ekstrak Link & Shortcode",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          100,
          300
        ]
      },
      {
        "parameters": {
          "conditions": {
            "options": {
              "caseSensitive": true,
              "leftValue": "",
              "typeValidation": "loose",
              "version": 2
            },
            "conditions": [
              {
                "id": "ada-ig",
                "leftValue": "={{ $json.instagram.length }}",
                "rightValue": 0,
                "operator": {
                  "type": "number",
                  "operation": "gt"
                }
              }
            ],
            "combinator": "and"
          },
          "options": {}
        },
        "id": "if-ada-ig",
        "name": "Ada URL Instagram?",
        "type": "n8n-nodes-base.if",
        "typeVersion": 2.2,
        "position": [
          320,
          300
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://api.apify.com/v2/acts/patient_discovery~instagram-reel-analytics-by-url/run-sync-get-dataset-items",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpQueryAuth",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ postUrls: $json.ig_urls }) }}",
          "options": {
            "timeout": 180000
          }
        },
        "id": "apify-scraper",
        "name": "Apify — Instagram Reel Analytics",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          540,
          180
        ],
        "credentials": {
          "httpQueryAuth": {
            "id": "NPdRlHHRhKiecZi5",
            "name": "Apify Token"
          }
        },
        "retryOnFail": true,
        "maxTries": 2,
        "waitBetweenTries": 5000,
        "notesInFlow": true,
        "notes": "Actor: patient_discovery~instagram-reel-analytics-by-url (bukan apify~instagram-reel-scraper)"
      },
      {
        "parameters": {
          "jsCode": "// Memetakan hasil scrape ke deliverable_id lewat field `code` (shortcode Instagram),\n// lalu menggabungkan angka segar ke payload supaya Math Engine memakai data terbaru.\nconst ext = $('Ekstrak Link & Shortcode').first().json;\nconst payload = ext.payload;\n\nconst peta = {};\nfor (const ig of ext.instagram) peta[ig.shortcode] = ig;\n\nlet hasil = [];\ntry {\n  hasil = $('Apify — Instagram Reel Analytics').all().map(i => i.json);\n} catch (e) {\n  hasil = [];   // cabang Apify tidak dijalankan (tidak ada URL Instagram)\n}\n\nconst sync_items = [];\nconst tak_cocok = [];\n\nfor (const post of hasil) {\n  const code = post.code;\n  const map = code ? peta[code] : null;\n  if (!map) { tak_cocok.push({ code: code || null, alasan: 'shortcode tidak cocok' }); continue; }\n\n  const m = post.metrics || {};\n  const item = {\n    deliverable_id: map.deliverable_id,\n    views: Number(m.play_count ?? m.view_count ?? 0) || 0,\n    likes: Number(m.like_count || 0),\n    comments: Number(m.comment_count || 0),\n  };\n  // Penting: shares & saves TIDAK tersedia dari actor ini.\n  // Jangan kirim 0 — itu akan menimpa data asli di database.\n  if (m.share_count != null) item.shares = Number(m.share_count);\n  if (m.save_count != null) item.saves = Number(m.save_count);\n\n  sync_items.push(item);\n}\n\nconst segar = {};\nfor (const s of sync_items) segar[s.deliverable_id] = s;\n\nfor (const d of (payload.deliverables || [])) {\n  const f = segar[d.id];\n  if (!f) continue;\n  d.metrics = d.metrics || {};\n  d.metrics.views = f.views;\n  d.metrics.likes = f.likes;\n  d.metrics.comments = f.comments;\n  if (f.shares != null) d.metrics.shares = f.shares;\n  if (f.saves != null) d.metrics.saves = f.saves;\n  d.metrics.engagements = f.likes + f.comments + (d.metrics.shares || 0) + (d.metrics.saves || 0);\n  d.metrics.er_percent = f.views > 0\n    ? Number(((d.metrics.engagements / f.views) * 100).toFixed(2)) : 0;\n  d.metrics.baru_di_scrape = true;\n}\n\nreturn [{\n  json: {\n    event_id: ext.event_id,\n    sync_items,\n    payload,\n    catatan: {\n      url_dilewati: ext.dilewati,\n      tiktok_belum_ditangani: ext.tiktok,\n      hasil_tak_cocok: tak_cocok,\n      jumlah_ter_scrape: sync_items.length,\n    },\n  },\n}];",
          "mode": "runOnceForAllItems"
        },
        "id": "petakan-&-gabungkan-metrik",
        "name": "Petakan & Gabungkan Metrik",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          760,
          300
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "=https://impactview.mdnetwork.web.id/api/v1/n8n/events/{{ $json.event_id }}/sync-scraped-metrics",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ scraped_items: $json.sync_items }) }}",
          "options": {
            "timeout": 60000
          }
        },
        "id": "sync-metrics",
        "name": "Simpan Metrik ke ImpactView",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1020,
          120
        ],
        "credentials": {
          "httpHeaderAuth": {
            "id": "6jYfhBUaEAyf7Q6l",
            "name": "ImpactView API"
          }
        },
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000
      },
      {
        "parameters": {
          "jsCode": "/**\n * ENGINE MATEMATIKA ANALITIK (v2.1 — diperbaiki)\n * Perbaikan: `const totalViewsSo Far` (ada spasi) adalah SyntaxError dan\n * membuat node ini selalu gagal. Sekarang `totalViewsSoFar`.\n */\nconst data = $('Petakan & Gabungkan Metrik').first().json;\nconst payload = data.payload;\nconst event = payload.event || {};\nconst deliverables = payload.deliverables || [];\nconst targets = event.targets || [];\n\nconst now = new Date();\nconst endDate = new Date(event.end_date || now);\nconst remainingDays = Math.max(1, Math.ceil((endDate - now) / (1000 * 60 * 60 * 24)));\n\nconst totalViewsSoFar = deliverables.reduce((acc, d) => acc + ((d.metrics && d.metrics.views) || 0), 0);\nconst totalDealFee = deliverables.reduce((acc, d) => acc + (d.deal_fee_allocated || 0), 0);\nconst overallCPV = totalViewsSoFar > 0 ? Math.round(totalDealFee / totalViewsSoFar) : 0;\n\nconst analyzedDeliverables = deliverables.map(d => {\n  const m = d.metrics || {};\n  const views = m.views || 0;\n  const saves = m.saves || 0;\n  const shares = m.shares || 0;\n  const likes = m.likes || 0;\n  const comments = m.comments || 0;\n  const fee = d.deal_fee_allocated || 0;\n\n  const cpv = views > 0 ? Math.round(fee / views) : 0;\n\n  // Resonance Index: Save (x4) + Share (x5) + Comment (x2) per view.\n  // CATATAN: actor Apify yang dipakai TIDAK menyediakan saves & shares.\n  // Jadi bobot x4/x5 praktis nol kecuali metrik itu diisi manual di dashboard.\n  const resonanceIndex = views > 0\n    ? Number((((saves * 4) + (shares * 5) + (comments * 2)) / views * 100).toFixed(2))\n    : 0;\n\n  const velocity = m.view_velocity || 0;\n  let healthStatus = 'Normal';\n  if (views > 1000 && velocity < 50) {\n    healthStatus = 'STAGNANT_FLATLINE';\n  } else if (resonanceIndex >= 3.5) {\n    healthStatus = 'HIGH_VIRAL_POTENTIAL';\n  } else if (cpv > 150) {\n    healthStatus = 'OVERPRICED_UNDERPERFORM';\n  }\n\n  return {\n    deliverable_id: d.id,\n    kol_id: d.kol_id,\n    kol_name: d.kol_name,\n    kol_category: d.kol_category,\n    platform: d.platform,\n    content_format: d.content_format,\n    tracking_url: d.tracking_url,\n    views, engagements: m.engagements || 0, er_percent: m.er_percent || 0,\n    saves, shares, cpv_idr: cpv, deal_fee: fee,\n    resonance_index: resonanceIndex,\n    view_velocity: velocity,\n    diagnostic_status: healthStatus,\n    metrik_baru_di_scrape: !!m.baru_di_scrape,\n  };\n});\n\nconst bestForAds = [...analyzedDeliverables]\n  .filter(d => d.views > 0)\n  .sort((a, b) => b.resonance_index - a.resonance_index)\n  .slice(0, 3);\n\nconst needRescue = [...analyzedDeliverables]\n  .filter(d => d.diagnostic_status === 'STAGNANT_FLATLINE' || d.diagnostic_status === 'OVERPRICED_UNDERPERFORM')\n  .slice(0, 3);\n\nreturn [{\n  json: {\n    event_id: event.id,\n    event_name: event.name,\n    event_description: event.description,\n    remaining_days: remainingDays,\n    total_current_views: totalViewsSoFar,\n    overall_cpv_idr: overallCPV,\n    total_budget_kol: event.budget_kol,\n    targets,\n    best_for_ads: bestForAds,\n    need_rescue_content: needRescue,\n    all_analyzed_items: analyzedDeliverables,\n    catatan_data: data.catatan,\n  },\n}];"
        },
        "id": "math-engine-(roi,-cpv,-velocity)",
        "name": "Math Engine (ROI, CPV, Velocity)",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1020,
          420
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "https://api.commandcode.ai/provider/v1/chat/completions",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({\n  model: \"deepseek/deepseek-v4-flash\",\n  messages: [\n    { role: \"system\", content: \"Kamu adalah Elite Growth Marketing Director & Paid Traffic Specialist (Media Buyer) tingkat tertinggi untuk Event & Brand Activation.\\nTugasmu adalah membedah data performa KOL hingga ke akar-akarnya, menemukan ROOT CAUSE masalah, dan merumuskan:\\n\\n1. REKOMENDASI ADS BOOSTING (TikTok Spark Ads / Meta Whitelist Ads):\\n   - Pilihlah deliverable dengan Resonance Index tertinggi.\\n   - Tentukan Objective kampanye (Video Views, Reach, atau Traffic).\\n   - Hitung estimasi budget ideal per hari selama 3-5 hari ke depan.\\n   - Tentukan Parameter Target Audiens (Usia, Demografi kota tier 1/2, Interest spesifik).\\n   - Berikan Copywriting Hook Iklan teruji yang melipatgandakan Watch Time.\\n\\n2. TAKTIK OUT-OF-THE-BOX ORGANIK (Bukan klise, tapi trik algoritma nyata):\\n   - Pinned Controversial / Curious Comment Hook di 60 menit pertama.\\n   - Audio Hijacking / Replacement (mengganti sound reels yang sedang drop ke sound trending).\\n   - Stitch / Duet Arbitrage (meminta KOL lain dengan views tinggi me-remix konten yang stagnan).\\n   - Instagram Story Interactive Funnel (Countdown sticker + Link sticker teaser).\\n\\nOUTPUT WAJIB JSON MURNI DENGAN FORMAT BERIKUT (Tanpa Markdown/Teks Pembuka):\\n{\\n  \\\"recommendations\\\": [\\n    {\\n      \\\"deliverable_id\\\": 1,\\n      \\\"kol_id\\\": 2,\\n      \\\"type\\\": \\\"boost_ads\\\" | \\\"out_of_box_tactic\\\",\\n      \\\"priority\\\": \\\"urgent\\\" | \\\"high\\\" | \\\"medium\\\",\\n      \\\"title\\\": \\\"Judul Taktik / Boosting Tajam\\\",\\n      \\\"summary\\\": \\\"Root cause analysis mendalam: kenapa angka ini terjadi dan kenapa strategi ini wajib diambil\\\",\\n      \\\"action_steps\\\": [\\n        \\\"Langkah taktis 1 yang detail\\\",\\n        \\\"Langkah taktis 2\\\",\\n        \\\"Langkah taktis 3\\\"\\n      ],\\n      \\\"ads_blueprint\\\": {\\n        \\\"recommended_budget\\\": \\\"Rp 75.000 - Rp 150.000 / hari\\\",\\n        \\\"objective\\\": \\\"Engagement / Video Views\\\",\\n        \\\"target_audience\\\": \\\"Interest, Usia, Lokasi\\\",\\n        \\\"copy_hook\\\": \\\"Teks pembuka/hook\\\",\\n        \\\"rationale\\\": \\\"Alasan matematis pemilihan konten ini\\\"\\n      }\\n    }\\n  ]\\n}\" },\n    { role: \"user\", content: `Bedah data performa event ini sampai ke akarnya.\n\nEvent: ${$json.event_name}\nDeskripsi: ${$json.event_description}\nSisa Waktu: ${$json.remaining_days} hari\nTotal Views: ${$json.total_current_views}\nOverall CPV: Rp ${$json.overall_cpv_idr} per view\nBudget KOL: Rp ${$json.total_budget_kol}\nTarget KPI: ${JSON.stringify($json.targets)}\n\nKANDIDAT KONTEN EMAS (Resonance tertinggi):\n${JSON.stringify($json.best_for_ads, null, 2)}\n\nKONTEN STAGNAN / BONCOS:\n${JSON.stringify($json.need_rescue_content, null, 2)}\n\nCATATAN KUALITAS DATA:\n${JSON.stringify($json.catatan_data, null, 2)}\n\nSEMUA DELIVERABLE:\n${JSON.stringify($json.all_analyzed_items, null, 2)}\n\nBuat minimal 2 Ads Blueprint dan 2 Taktik Algoritma Out-of-the-Box yang eksekusional.` }\n  ],\n  response_format: { type: \"json_object\" },\n  stream: false\n}) }}",
          "options": {
            "timeout": 180000
          }
        },
        "id": "ai-reasoning",
        "name": "Deep AI Reasoning (Command Code)",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1260,
          420
        ],
        "credentials": {
          "httpHeaderAuth": {
            "id": "4pZCab9psqQKX2GY",
            "name": "Command Code API"
          }
        },
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 5000,
        "notesInFlow": true,
        "notes": "gpt-4o tidak ada di Command Code. Diganti deepseek/deepseek-v4-flash + response_format json_object."
      },
      {
        "parameters": {
          "jsCode": "// Model membalas JSON dalam bentuk string di choices[0].message.content.\nconst res = $input.first().json;\nconst math = $('Math Engine (ROI, CPV, Velocity)').first().json;\n\nconst content = res.choices?.[0]?.message?.content ?? '';\nlet parsed = null;\nlet galat = null;\ntry {\n  parsed = JSON.parse(content);\n} catch (e) {\n  galat = e.message;\n}\n\nlet rekomendasi = parsed?.recommendations ?? parsed?.recommendation ?? [];\nif (!Array.isArray(rekomendasi)) rekomendasi = [];\n\nreturn [{\n  json: {\n    event_id: math.event_id,\n    jumlah: rekomendasi.length,\n    recommendations: rekomendasi,\n    galat_parse: galat,\n    cuplikan: String(content).slice(0, 400),\n  },\n}];"
        },
        "id": "baca-keluaran-ai",
        "name": "Baca Keluaran AI",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          1480,
          420
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "=https://impactview.mdnetwork.web.id/api/v1/n8n/events/{{ $json.event_id }}/recommendations",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ recommendations: $json.recommendations }) }}",
          "options": {
            "timeout": 60000
          }
        },
        "id": "dispatch",
        "name": "Kirim Rekomendasi ke ImpactView",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          1700,
          420
        ],
        "credentials": {
          "httpHeaderAuth": {
            "id": "6jYfhBUaEAyf7Q6l",
            "name": "ImpactView API"
          }
        },
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000
      }
    ],
    "connections": {
      "Cron (tengah malam WIB)": {
        "main": [
          [
            {
              "node": "Ambil Daftar Event Aktif",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Manual Trigger (jalankan sekarang)": {
        "main": [
          [
            {
              "node": "Ambil Daftar Event Aktif",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Webhook (dari Dashboard)": {
        "main": [
          [
            {
              "node": "Siapkan Event",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Ambil Daftar Event Aktif": {
        "main": [
          [
            {
              "node": "Siapkan Event",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Siapkan Event": {
        "main": [
          [
            {
              "node": "Ambil Performa Event",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Ambil Performa Event": {
        "main": [
          [
            {
              "node": "Ekstrak Link & Shortcode",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Ekstrak Link & Shortcode": {
        "main": [
          [
            {
              "node": "Ada URL Instagram?",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Apify — Instagram Reel Analytics": {
        "main": [
          [
            {
              "node": "Petakan & Gabungkan Metrik",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Math Engine (ROI, CPV, Velocity)": {
        "main": [
          [
            {
              "node": "Deep AI Reasoning (Command Code)",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Deep AI Reasoning (Command Code)": {
        "main": [
          [
            {
              "node": "Baca Keluaran AI",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Baca Keluaran AI": {
        "main": [
          [
            {
              "node": "Kirim Rekomendasi ke ImpactView",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Petakan & Gabungkan Metrik": {
        "main": [
          [
            {
              "node": "Simpan Metrik ke ImpactView",
              "type": "main",
              "index": 0
            },
            {
              "node": "Math Engine (ROI, CPV, Velocity)",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Ada URL Instagram?": {
        "main": [
          [
            {
              "node": "Apify — Instagram Reel Analytics",
              "type": "main",
              "index": 0
            }
          ],
          [
            {
              "node": "Petakan & Gabungkan Metrik",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "settings": {
      "executionOrder": "v1",
      "saveManualExecutions": true,
      "callerPolicy": "workflowsFromSameOwner",
      "timezone": "Asia/Jakarta"
    },
    "staticData": {
      "node:Cron (tengah malam WIB)": {
        "recurrenceRules": [
          null
        ],
        "recurrenceRuleSignatures": [
          null
        ]
      }
    },
    "meta": null,
    "nodeGroups": [],
    "pinData": {},
    "versionId": "05411dd1-cc01-41e3-b333-e2a8ab4e2596",
    "activeVersionId": null,
    "versionCounter": 5,
    "triggerCount": 2,
    "sourceWorkflowId": null,
    "tags": [],
    "shared": [
      {
        "updatedAt": "2026-10-21T22:10:17.223Z",
        "createdAt": "2026-10-21T22:10:17.223Z",
        "role": "workflow:owner",
        "workflowId": "HVucE5zuj9mR8H6k",
        "projectId": "O5dS2R3vFvvVOZZK",
        "project": {
          "updatedAt": "2026-10-23T06:20:42.590Z",
          "createdAt": "2026-10-21T04:53:49.401Z",
          "id": "O5dS2R3vFvvVOZZK",
          "name": "Mohammad Khoiruman <arifkhoiruman2@gmail.com>",
          "type": "personal",
          "icon": null,
          "description": null,
          "customTelemetryTags": [],
          "creatorId": "77323fa2-5a74-405e-999c-961acc6ca1ba"
        }
      }
    ],
    "versionMetadata": {
      "name": null,
      "description": null
    }
  },
  {
    "updatedAt": "2026-10-21T21:47:10.347Z",
    "createdAt": "2026-10-21T08:19:14.923Z",
    "id": "l9kx8WlAcxkZ2x9c",
    "name": "Chatbot API - Command Code",
    "description": null,
    "active": true,
    "isArchived": false,
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "chat",
          "responseMode": "responseNode",
          "options": {
            "allowedOrigins": "*"
          },
          "authentication": "headerAuth"
        },
        "id": "aa000000-0000-4000-8000-000000000001",
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          -460,
          0
        ],
        "webhookId": "aa000000-0000-4000-8000-0000000000f1",
        "credentials": {
          "httpHeaderAuth": {
            "id": "mVCkEw5sWtGqwfiz",
            "name": "Webhook Auth - Chatbot"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Susun payload untuk Command Code Provider API (format OpenAI Chat Completions)\nconst input = $input.first().json;\nconst body = input.body ?? {};\nconst query = input.query ?? {};\n\nconst sessionId = String(body.sessionId ?? query.sessionId ?? 'default');\nconst userMessage = String(body.message ?? query.message ?? '').trim();\n\nif (!userMessage) {\n  throw new Error('Field \"message\" wajib diisi.');\n}\n\nconst model = body.model ?? $env.CHAT_MODEL ?? 'deepseek/deepseek-v4-flash';\nconst systemPrompt = body.system ?? $env.SYSTEM_PROMPT ?? 'Kamu adalah asisten AI yang membantu.';\n\n// Memori percakapan per-session. Persisten hanya pada eksekusi produksi\n// (webhook production), tidak tersimpan saat klik 'Test workflow'.\nconst staticData = $getWorkflowStaticData('global');\nstaticData.memory = staticData.memory || {};\nconst stored = staticData.memory[sessionId] || [];\n\nconst history = Array.isArray(body.history)\n  ? body.history\n  : stored.slice(-10);\n\nconst messages = [\n  { role: 'system', content: systemPrompt },\n  ...history,\n  { role: 'user', content: userMessage },\n];\n\nreturn [{ json: { sessionId, model, messages, userMessage } }];"
        },
        "id": "aa000000-0000-4000-8000-000000000002",
        "name": "Build Request",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          -240,
          0
        ]
      },
      {
        "parameters": {
          "method": "POST",
          "url": "={{ $env.CMD_API_BASE + '/chat/completions' }}",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth",
          "sendBody": true,
          "specifyBody": "json",
          "jsonBody": "={{ JSON.stringify({ model: $json.model, messages: $json.messages, stream: false }) }}",
          "options": {
            "timeout": 120000,
            "response": {
              "response": {
                "neverError": false
              }
            }
          }
        },
        "id": "aa000000-0000-4000-8000-000000000003",
        "name": "Command Code API",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          0,
          0
        ],
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000,
        "onError": "continueRegularOutput",
        "credentials": {
          "httpHeaderAuth": {
            "id": "4pZCab9psqQKX2GY",
            "name": "Command Code API"
          }
        }
      },
      {
        "parameters": {
          "jsCode": "// Ambil jawaban model, simpan ke memori, siapkan respons HTTP.\nconst staticData = $getWorkflowStaticData('global');\nstaticData.memory = staticData.memory || {};\n\nconst req = $('Build Request').item.json;\nconst res = $input.first().json;\n\nconst upstreamError = res.error;\nconst reply = upstreamError\n  ? `[error] ${upstreamError.message ?? JSON.stringify(upstreamError)}`\n  : (res.choices?.[0]?.message?.content ?? '').trim();\n\nif (!upstreamError && reply) {\n  const history = staticData.memory[req.sessionId] || [];\n  history.push({ role: 'user', content: req.userMessage });\n  history.push({ role: 'assistant', content: reply });\n  staticData.memory[req.sessionId] = history.slice(-20);\n}\n\nreturn [{\n  json: {\n    sessionId: req.sessionId,\n    model: req.model,\n    reply,\n    error: upstreamError ? true : false,\n    usage: res.usage ?? null,\n  },\n}];"
        },
        "id": "aa000000-0000-4000-8000-000000000004",
        "name": "Extract Reply",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          220,
          0
        ]
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify({ sessionId: $json.sessionId, reply: $json.reply, error: $json.error, usage: $json.usage }) }}",
          "options": {
            "responseCode": "={{ $json.error ? 502 : 200 }}"
          }
        },
        "id": "aa000000-0000-4000-8000-000000000005",
        "name": "Respond to Webhook",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          440,
          0
        ]
      }
    ],
    "connections": {
      "Webhook": {
        "main": [
          [
            {
              "node": "Build Request",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Build Request": {
        "main": [
          [
            {
              "node": "Command Code API",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Command Code API": {
        "main": [
          [
            {
              "node": "Extract Reply",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Extract Reply": {
        "main": [
          [
            {
              "node": "Respond to Webhook",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "settings": {
      "executionOrder": "v1",
      "saveManualExecutions": true,
      "callerPolicy": "workflowsFromSameOwner"
    },
    "staticData": {
      "global": {
        "memory": {
          "fin-1": [
            {
              "role": "user",
              "content": "Jawab satu kata: hidup"
            },
            {
              "role": "assistant",
              "content": "Hidup"
            },
            {
              "role": "user",
              "content": "Aku tadi minta jawab apa?"
            },
            {
              "role": "assistant",
              "content": "Kamu minta aku menjawab satu kata: hidup."
            }
          ],
          "pub-1": [
            {
              "role": "user",
              "content": "Jawab satu kata: publik"
            },
            {
              "role": "assistant",
              "content": "publik"
            }
          ],
          "default": [
            {
              "role": "user",
              "content": "hello"
            },
            {
              "role": "assistant",
              "content": "Halo! Ada yang bisa saya bantu?"
            }
          ],
          "cron-cek": [
            {
              "role": "user",
              "content": "Jawab satu kata: hidup"
            },
            {
              "role": "assistant",
              "content": "Hidup"
            }
          ],
          "pw-reset": [
            {
              "role": "user",
              "content": "Jawab satu kata: hidup"
            },
            {
              "role": "assistant",
              "content": "hidup"
            }
          ]
        }
      }
    },
    "meta": null,
    "nodeGroups": [],
    "pinData": {},
    "versionId": "d422bce9-f2b6-4bce-997f-553296818fd1",
    "activeVersionId": "d422bce9-f2b6-4bce-997f-553296818fd1",
    "versionCounter": 5,
    "triggerCount": 1,
    "sourceWorkflowId": null,
    "tags": [],
    "shared": [
      {
        "updatedAt": "2026-10-21T08:19:14.923Z",
        "createdAt": "2026-10-21T08:19:14.923Z",
        "role": "workflow:owner",
        "workflowId": "l9kx8WlAcxkZ2x9c",
        "projectId": "O5dS2R3vFvvVOZZK",
        "project": {
          "updatedAt": "2026-10-23T06:20:42.590Z",
          "createdAt": "2026-10-21T04:53:49.401Z",
          "id": "O5dS2R3vFvvVOZZK",
          "name": "Mohammad Khoiruman <arifkhoiruman2@gmail.com>",
          "type": "personal",
          "icon": null,
          "description": null,
          "customTelemetryTags": [],
          "creatorId": "77323fa2-5a74-405e-999c-961acc6ca1ba"
        }
      }
    ],
    "versionMetadata": {
      "name": null,
      "description": null
    }
  },
  {
    "updatedAt": "2026-10-23T06:59:02.982Z",
    "createdAt": "2026-10-23T06:52:11.168Z",
    "id": "WKCVyMQBetXh8FFk",
    "name": "ImpactView — Sheet Sync Preview",
    "description": null,
    "active": true,
    "isArchived": false,
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "sheet-preview",
          "responseMode": "responseNode",
          "authentication": "headerAuth",
          "options": {}
        },
        "id": "sheet-webhook",
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          -620,
          0
        ],
        "webhookId": "ee000000-0000-4000-8000-0000000000f1",
        "credentials": {
          "httpHeaderAuth": {
            "id": "nvkabokeng6itHSl",
            "name": "ImpactView n8n Webhook"
          }
        }
      },
      {
        "parameters": {
          "url": "https://docs.google.com/spreadsheets/d/1kKPyGWZZKS-5TtCSXBtQwX_GwxnPybUlp8FmWgEjcvM/gviz/tq?tqx=out:csv&sheet=NANO",
          "options": {
            "timeout": 60000,
            "response": {
              "response": {
                "responseFormat": "text"
              }
            }
          }
        },
        "id": "sheet-nano",
        "name": "Sheet NANO",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -380,
          -180
        ],
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000,
        "notesInFlow": true,
        "notes": "CSV publik tab NANO"
      },
      {
        "parameters": {
          "url": "https://docs.google.com/spreadsheets/d/1kKPyGWZZKS-5TtCSXBtQwX_GwxnPybUlp8FmWgEjcvM/gviz/tq?tqx=out:csv&sheet=MICRO",
          "options": {
            "timeout": 60000,
            "response": {
              "response": {
                "responseFormat": "text"
              }
            }
          }
        },
        "id": "sheet-micro",
        "name": "Sheet MICRO",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -380,
          -20
        ],
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000,
        "notesInFlow": true,
        "notes": "CSV publik tab MICRO"
      },
      {
        "parameters": {
          "url": "https://docs.google.com/spreadsheets/d/1kKPyGWZZKS-5TtCSXBtQwX_GwxnPybUlp8FmWgEjcvM/gviz/tq?tqx=out:csv&sheet=MACRO",
          "options": {
            "timeout": 60000,
            "response": {
              "response": {
                "responseFormat": "text"
              }
            }
          }
        },
        "id": "sheet-macro",
        "name": "Sheet MACRO",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -380,
          140
        ],
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000,
        "notesInFlow": true,
        "notes": "CSV publik tab MACRO"
      },
      {
        "parameters": {
          "url": "=https://impactview.mdnetwork.web.id/api/v1/n8n/events/{{ $('Webhook').first().json.body.event_id }}/kol-index",
          "authentication": "genericCredentialType",
          "genericAuthType": "httpHeaderAuth",
          "options": {
            "timeout": 60000
          }
        },
        "id": "kol-index",
        "name": "KOL Index",
        "type": "n8n-nodes-base.httpRequest",
        "typeVersion": 4.2,
        "position": [
          -140,
          300
        ],
        "credentials": {
          "httpHeaderAuth": {
            "id": "6jYfhBUaEAyf7Q6l",
            "name": "ImpactView API"
          }
        },
        "retryOnFail": true,
        "maxTries": 3,
        "waitBetweenTries": 3000
      },
      {
        "parameters": {
          "jsCode": "\n// ============================================================================\n// Normalisasi + diff antara Google Sheet dan data KOL di ImpactView.\n// Tidak menulis apa pun ke database.\n// ============================================================================\n\nconst eventId = Number($('Webhook').first().json.body?.event_id ?? 0);\nif (!eventId) throw new Error('event_id wajib dikirim di body.');\n\nconst csvNano = $('Sheet NANO').first().json.data ?? '';\nconst csvMicro = $('Sheet MICRO').first().json.data ?? '';\nconst csvMacro = $('Sheet MACRO').first().json.data ?? '';\nconst index = $('KOL Index').first().json ?? {};\n\nconst peringatan = [];\n\n// ---------------------------------------------------------------- util\nfunction parseCsv(text) {\n  const rows = [];\n  let row = [], field = '', inQ = false;\n  for (let i = 0; i < text.length; i++) {\n    const c = text[i];\n    if (inQ) {\n      if (c === '\"') {\n        if (text[i + 1] === '\"') { field += '\"'; i++; }\n        else inQ = false;\n      } else field += c;\n    } else if (c === '\"') inQ = true;\n    else if (c === ',') { row.push(field); field = ''; }\n    else if (c === '\\n') { row.push(field); rows.push(row); row = []; field = ''; }\n    else if (c !== '\\r') field += c;\n  }\n  if (field.length || row.length) { row.push(field); rows.push(row); }\n  return rows.filter((r) => r.some((x) => String(x).trim() !== ''));\n}\n\nconst pick = (obj, ...names) => {\n  for (const n of names) {\n    const k = Object.keys(obj).find((x) => x.trim().toLowerCase() === n);\n    if (k !== undefined && String(obj[k]).trim() !== '') return String(obj[k]).trim();\n  }\n  return '';\n};\n\nfunction normPlatform(v) {\n  const s = String(v || '').trim().toLowerCase();\n  if (s.startsWith('insta')) return 'instagram';\n  if (s.startsWith('tik')) return 'tiktok';\n  if (s.startsWith('youtu')) return 'youtube';\n  if (s === 'x' || s === 'twitter') return 'x';\n  if (s.startsWith('face')) return 'facebook';\n  return s || null;\n}\n\nfunction extractUsername(raw, platform) {\n  let s = String(raw || '').trim();\n  if (!s) return '';\n  s = s.replace(/^Https?/i, 'https');\n  // rapikan double slash SETELAH domain: \"instagram.com//user\" -> \"instagram.com/user\"\n  s = s.replace('://', '\\u0000').replace(/\\/{2,}/g, '/').replace('\\u0000', '://');\n  const m = s.match(/(?:instagram\\.com|tiktok\\.com)\\/@?([A-Za-z0-9_.]+)/i);\n  if (m) return m[1].toLowerCase();\n  return s.replace(/^@/, '').replace(/\\/+$/, '').split('?')[0].trim().toLowerCase();\n}\n\nfunction parseFollowers(v) {\n  let s = String(v == null ? '' : v).trim().toLowerCase().replace(/\\s/g, '');\n  if (!s) return null;\n\n  // \"1,535\" -> \"1535\"\n  s = s.replace(/,/g, '');\n\n  let mult = 1;\n  if (s.endsWith('k')) { mult = 1000; s = s.slice(0, -1); }\n  else if (s.endsWith('m')) { mult = 1000000; s = s.slice(0, -1); }\n\n  // Tanpa suffix, titik = pemisah ribuan gaya Indonesia: \"1.535\" -> 1535.\n  // Dengan suffix, titik tetap desimal: \"2.1m\" -> 2.1 * 1e6.\n  if (mult === 1 && /^\\d{1,3}(\\.\\d{3})+$/.test(s)) {\n    s = s.replace(/\\./g, '');\n  }\n\n  const n = parseFloat(s);\n  if (isNaN(n)) return { ambigu: true, mentah: v };\n  return Math.round(n * mult);\n}\n\n// Kunci postingan: tahan terhadap perubahan parameter tracking (?stkn, utm_*)\nfunction postKey(url) {\n  const u = String(url || '').trim();\n  if (!u) return null;\n  let m = u.match(/instagram\\.com\\/(reel|reels|p|tv)\\/([A-Za-z0-9_-]+)/i);\n  if (m) return 'ig:' + m[2];\n  m = u.match(/tiktok\\.com\\/.*?\\/video\\/(\\d+)/i);\n  if (m) return 'tt:' + m[1];\n  m = u.match(/vt\\.tiktok\\.com\\/([A-Za-z0-9]+)/i);\n  if (m) return 'tt_short:' + m[1];\n  return 'url:' + u.split('?')[0].replace(/\\/+$/, '').toLowerCase();\n}\n\nfunction canonicalPost(url) {\n  const u = String(url || '').trim();\n  const m = u.match(/instagram\\.com\\/(reel|reels|p|tv)\\/([A-Za-z0-9_-]+)/i);\n  if (m) {\n    const type = m[1].toLowerCase() === 'reels' ? 'reel' : m[1].toLowerCase();\n    return `https://www.instagram.com/${type}/${m[2]}/`;\n  }\n  return u.split('?')[0].replace(/\\/+$/, '');\n}\n\nfunction parseDate(v) {\n  const s = String(v || '').trim();\n  let m = s.match(/^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{4})$/);\n  if (m) return `${m[3]}-${m[2].padStart(2, '0')}-${m[1].padStart(2, '0')}`;\n  m = s.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n  return m ? s : null;\n}\n\n// ---------------------------------------------------------------- baca sheet\nconst SHEETS = [\n  { name: 'NANO', csv: csvNano },\n  { name: 'MICRO', csv: csvMicro },\n  { name: 'MACRO', csv: csvMacro },\n];\n\nconst sheetRows = [];\n\nfor (const sh of SHEETS) {\n  const rows = parseCsv(sh.csv);\n  if (rows.length < 2) continue;\n  const header = rows[0].map((h) => String(h).trim());\n  for (let i = 1; i < rows.length; i++) {\n    const obj = {};\n    header.forEach((h, idx) => { if (h) obj[h] = rows[i][idx] ?? ''; });\n\n    const platform = normPlatform(pick(obj, 'platform'));\n    if (!platform) continue;\n\n    const linkPlatform = pick(obj, 'link platform');\n    const username = extractUsername(linkPlatform, platform);\n    if (!username) continue;\n\n    // Tier: kolom \"Column 15\" kalau ada, kalau tidak dari nama tab\n    let tier = pick(obj, 'column 15').toLowerCase();\n    if (!['mega', 'macro', 'micro', 'nano'].includes(tier)) tier = sh.name.toLowerCase();\n\n    const linkUpload = pick(obj, 'link upload', 'link upload (instagram)');\n    const key = postKey(linkUpload);\n    const followers = parseFollowers(pick(obj, 'follower'));\n\n    const baris = i + 1;\n    if (followers && followers.ambigu) {\n      peringatan.push({ tab: sh.name, baris, masalah: `FOLLOWER tidak bisa dibaca: \"${followers.mentah}\"` });\n    }\n    if (linkUpload && key && key.startsWith('ig:') && !/instagram\\.com\\/(reel|p|tv)/i.test(linkUpload)) {\n      peringatan.push({ tab: sh.name, baris, masalah: 'LINK UPLOAD tidak dikenali bentuknya' });\n    }\n    if (linkUpload && extractUsername(linkUpload) === username && !key) {\n      peringatan.push({ tab: sh.name, baris, masalah: 'LINK UPLOAD menunjuk profil, bukan postingan' });\n    }\n\n    sheetRows.push({\n      tab: sh.name,\n      baris,\n      nama: pick(obj, 'nama kol'),\n      platform,\n      username,\n      tier,\n      followers: followers && !followers.ambigu ? followers : null,\n      link_upload: linkUpload || null,\n      post_key: key,\n      tanggal_upload: parseDate(pick(obj, 'tanggal upload')),\n    });\n  }\n}\n\n// ---------------------------------------------------------------- index DB\nconst dbPlatforms = (index.platforms || []).map((p) => ({\n  ...p,\n  key: `${p.platform}|${String(p.username || '').toLowerCase()}`,\n  postKeys: (p.deliverables || []).map((d) => postKey(d.live_post_url)).filter(Boolean),\n}));\n\nconst dbByKey = new Map(dbPlatforms.map((p) => [p.key, p]));\nconst sheetByKey = new Map();\nfor (const r of sheetRows) {\n  const k = `${r.platform}|${r.username}`;\n  if (!sheetByKey.has(k)) sheetByKey.set(k, []);\n  sheetByKey.get(k).push(r);\n}\n\n// ---------------------------------------------------------------- diff\nconst kol_baru = [];\nconst kol_takedown = [];\nconst link_baru = [];\nconst link_berubah = [];\nconst data_berubah = [];\nconst cocok = [];\n\nfor (const [key, rows] of sheetByKey) {\n  const db = dbByKey.get(key);\n\n  if (!db) {\n    const r = rows[0];\n    kol_baru.push({\n      platform: r.platform, username: r.username, nama: r.nama, tier: r.tier,\n      followers: r.followers, link_upload: r.link_upload,\n      jumlah_baris: rows.length, tab: r.tab,\n    });\n    continue;\n  }\n\n  cocok.push(key);\n\n  const dbKeys = new Set(db.postKeys);\n  const seen = new Set();\n\n  for (const r of rows) {\n    if (!r.post_key) continue;\n    if (seen.has(r.post_key)) continue;\n    seen.add(r.post_key);\n\n    if (!dbKeys.has(r.post_key)) {\n      link_baru.push({\n        kol_id: db.kol_id, event_kol_id: db.event_kol_id, kol_platform_id: db.kol_platform_id,\n        platform: r.platform, username: r.username, nama: r.nama,\n        link: canonicalPost(r.link_upload), post_key: r.post_key,\n        tanggal_upload: r.tanggal_upload, tab: r.tab, baris: r.baris,\n      });\n    } else {\n      const d = (db.deliverables || []).find((x) => postKey(x.live_post_url) === r.post_key);\n      if (d && d.live_post_url && canonicalPost(d.live_post_url) !== canonicalPost(r.link_upload)) {\n        link_berubah.push({\n          deliverable_id: d.id, username: r.username, platform: r.platform,\n          dari: d.live_post_url, ke: canonicalPost(r.link_upload),\n        });\n      }\n    }\n  }\n\n  const r = rows[0];\n  if (r.followers != null && db.followers != null && Number(r.followers) !== Number(db.followers)) {\n    data_berubah.push({\n      kol_platform_id: db.kol_platform_id, username: r.username, platform: r.platform,\n      field: 'followers', dari: db.followers, ke: r.followers,\n    });\n  }\n  if (r.tier && db.tier && r.tier !== db.tier) {\n    data_berubah.push({\n      kol_platform_id: db.kol_platform_id, username: r.username, platform: r.platform,\n      field: 'tier', dari: db.tier, ke: r.tier,\n    });\n  }\n}\n\nfor (const p of dbPlatforms) {\n  if (!sheetByKey.has(p.key)) {\n    kol_takedown.push({\n      kol_id: p.kol_id, event_kol_id: p.event_kol_id, kol_platform_id: p.kol_platform_id,\n      platform: p.platform, username: p.username, nama: p.nama, tier: p.tier,\n      jumlah_deliverable: (p.deliverables || []).length,\n    });\n  }\n}\n\n\n// Pasangkan kol_baru & kol_takedown yang nama KOL-nya sama pada platform yang\n// sama: itu hampir pasti username yang berubah, bukan dua masalah terpisah.\nconst ganti_username = [];\nconst normNama = (x) => String(x || '').toLowerCase().replace(/[^a-z0-9]/g, '');\n\nfor (let i = kol_baru.length - 1; i >= 0; i--) {\n  const b = kol_baru[i];\n  if (!normNama(b.nama)) continue;\n  const j = kol_takedown.findIndex(\n    (t) => t.platform === b.platform && normNama(t.nama) === normNama(b.nama),\n  );\n  if (j < 0) continue;\n  const t = kol_takedown[j];\n  ganti_username.push({\n    nama: b.nama,\n    platform: b.platform,\n    username_lama: t.username,\n    username_baru: b.username,\n    kol_id: t.kol_id,\n    kol_platform_id: t.kol_platform_id,\n    event_kol_id: t.event_kol_id,\n    tier: b.tier,\n    followers: b.followers,\n    link_upload: b.link_upload,\n  });\n  kol_baru.splice(i, 1);\n  kol_takedown.splice(j, 1);\n}\n\n// ---------------------------------------------------------------- keluaran\nreturn [{\n  json: {\n    event_id: eventId,\n    event_name: index.event?.name ?? null,\n    generated_at: new Date().toISOString(),\n    sumber: { spreadsheet_id: '1kKPyGWZZKS-5TtCSXBtQwX_GwxnPybUlp8FmWgEjcvM', tabs: SHEETS.map((s) => s.name) },\n    summary: {\n      baris_sheet: sheetRows.length,\n      platform_db: dbPlatforms.length,\n      cocok: cocok.length,\n      kol_baru: kol_baru.length,\n      kol_takedown: kol_takedown.length,\n      ganti_username: ganti_username.length,\n      link_baru: link_baru.length,\n      link_berubah: link_berubah.length,\n      data_berubah: data_berubah.length,\n      peringatan: peringatan.length,\n    },\n    kol_baru,\n    kol_takedown,\n    ganti_username,\n    link_baru,\n    link_berubah,\n    data_berubah,\n    peringatan: peringatan.slice(0, 50),\n  },\n}];\n",
          "mode": "runOnceForAllItems"
        },
        "id": "diff",
        "name": "Normalisasi & Diff",
        "type": "n8n-nodes-base.code",
        "typeVersion": 2,
        "position": [
          100,
          0
        ],
        "notesInFlow": true,
        "notes": "Tidak menulis ke DB. Hanya menghasilkan JSON preview."
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify($json) }}",
          "options": {
            "responseCode": 200
          }
        },
        "id": "respond",
        "name": "Respond to Webhook",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          340,
          0
        ]
      }
    ],
    "connections": {
      "Webhook": {
        "main": [
          [
            {
              "node": "Sheet NANO",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Sheet NANO": {
        "main": [
          [
            {
              "node": "Sheet MICRO",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Sheet MICRO": {
        "main": [
          [
            {
              "node": "Sheet MACRO",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Sheet MACRO": {
        "main": [
          [
            {
              "node": "KOL Index",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "KOL Index": {
        "main": [
          [
            {
              "node": "Normalisasi & Diff",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Normalisasi & Diff": {
        "main": [
          [
            {
              "node": "Respond to Webhook",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "settings": {
      "executionOrder": "v1",
      "saveManualExecutions": true,
      "callerPolicy": "workflowsFromSameOwner",
      "timezone": "Asia/Jakarta"
    },
    "staticData": null,
    "meta": null,
    "nodeGroups": [],
    "pinData": {},
    "versionId": "6a963439-acab-4a73-872f-4fc9121c4c4b",
    "activeVersionId": "6a963439-acab-4a73-872f-4fc9121c4c4b",
    "versionCounter": 3,
    "triggerCount": 1,
    "sourceWorkflowId": null,
    "tags": [],
    "shared": [
      {
        "updatedAt": "2026-10-23T06:52:11.168Z",
        "createdAt": "2026-10-23T06:52:11.168Z",
        "role": "workflow:owner",
        "workflowId": "WKCVyMQBetXh8FFk",
        "projectId": "O5dS2R3vFvvVOZZK",
        "project": {
          "updatedAt": "2026-10-23T06:20:42.590Z",
          "createdAt": "2026-10-21T04:53:49.401Z",
          "id": "O5dS2R3vFvvVOZZK",
          "name": "Mohammad Khoiruman <arifkhoiruman2@gmail.com>",
          "type": "personal",
          "icon": null,
          "description": null,
          "customTelemetryTags": [],
          "creatorId": "77323fa2-5a74-405e-999c-961acc6ca1ba"
        }
      }
    ],
    "versionMetadata": {
      "name": null,
      "description": null
    }
  },
  {
    "updatedAt": "2026-10-21T21:45:11.027Z",
    "createdAt": "2026-10-21T21:29:38.313Z",
    "id": "RBeftVKCMM3sApGD",
    "name": "Agent Dashboard - Command Code",
    "description": null,
    "active": true,
    "isArchived": false,
    "nodes": [
      {
        "parameters": {
          "httpMethod": "POST",
          "path": "agent",
          "responseMode": "responseNode",
          "options": {
            "allowedOrigins": "*"
          },
          "authentication": "headerAuth"
        },
        "id": "cc000000-0000-4000-8000-000000000001",
        "name": "Webhook",
        "type": "n8n-nodes-base.webhook",
        "typeVersion": 2,
        "position": [
          -620,
          0
        ],
        "webhookId": "cc000000-0000-4000-8000-0000000000f1",
        "credentials": {
          "httpHeaderAuth": {
            "id": "BmNYa8ZUOmDjn7aA",
            "name": "Webhook Auth - Agent"
          }
        }
      },
      {
        "parameters": {
          "promptType": "define",
          "text": "={{ $json.body.message }}",
          "options": {
            "systemMessage": "=Kamu adalah asisten yang membantu mengelola dashboard.\n\nAturan:\n- Selalu pakai tool yang tersedia untuk mengambil data. Jangan pernah mengarang angka atau isi data.\n- Kamu hanya punya akses BACA. Jangan mengaku sudah mengubah atau menghapus data.\n- Kalau data tidak bisa diambil lewat tool yang ada, katakan terus terang dan sebutkan tool apa yang kamu butuhkan.\n- Jawab ringkas dan jelas dalam Bahasa Indonesia."
          }
        },
        "id": "cc000000-0000-4000-8000-000000000002",
        "name": "AI Agent",
        "type": "@n8n/n8n-nodes-langchain.agent",
        "typeVersion": 3.1,
        "position": [
          -380,
          0
        ]
      },
      {
        "parameters": {
          "model": {
            "__rl": true,
            "mode": "id",
            "value": "deepseek/deepseek-v4-flash"
          },
          "responsesApiEnabled": false,
          "options": {}
        },
        "id": "cc000000-0000-4000-8000-000000000003",
        "name": "Command Code Chat Model",
        "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
        "typeVersion": 1.3,
        "position": [
          -560,
          240
        ],
        "credentials": {
          "openAiApi": {
            "id": "O15RAXfWFILGTy8N",
            "name": "Command Code (Chat Model)"
          }
        }
      },
      {
        "parameters": {
          "sessionIdType": "customKey",
          "sessionKey": "={{ $('Webhook').first().json.body.sessionId || 'default' }}",
          "contextWindowLength": 10
        },
        "id": "cc000000-0000-4000-8000-000000000004",
        "name": "Simple Memory",
        "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
        "typeVersion": 1.4,
        "position": [
          -340,
          240
        ]
      },
      {
        "parameters": {
          "name": "daftar_model_ai",
          "description": "Ambil daftar model AI yang tersedia beserta jumlahnya. Panggil tool ini saat pengguna bertanya soal model AI apa saja yang tersedia. GANTI isi code ini dengan panggilan ke API dashboard kamu.",
          "language": "javaScript",
          "jsCode": "// GANTI url di bawah dengan endpoint API dashboard kamu.\nconst res = await this.helpers.httpRequest({\n  method: 'GET',\n  url: 'https://api.commandcode.ai/provider/v1/models',\n  json: true,\n});\n\nconst items = Array.isArray(res.data) ? res.data : [];\nconst contoh = items.slice(0, 5).map((m) => m.id);\n\nreturn JSON.stringify({ jumlah_model: items.length, contoh });"
        },
        "id": "cc000000-0000-4000-8000-000000000005",
        "name": "Tool: Daftar Model AI",
        "type": "@n8n/n8n-nodes-langchain.toolCode",
        "typeVersion": 1.3,
        "position": [
          -120,
          240
        ]
      },
      {
        "parameters": {
          "respondWith": "json",
          "responseBody": "={{ JSON.stringify({ sessionId: $('Webhook').first().json.body.sessionId || 'default', reply: $json.output, error: false }) }}",
          "options": {}
        },
        "id": "cc000000-0000-4000-8000-000000000006",
        "name": "Respond to Webhook",
        "type": "n8n-nodes-base.respondToWebhook",
        "typeVersion": 1.1,
        "position": [
          -100,
          0
        ]
      }
    ],
    "connections": {
      "Webhook": {
        "main": [
          [
            {
              "node": "AI Agent",
              "type": "main",
              "index": 0
            }
          ]
        ]
      },
      "Command Code Chat Model": {
        "ai_languageModel": [
          [
            {
              "node": "AI Agent",
              "type": "ai_languageModel",
              "index": 0
            }
          ]
        ]
      },
      "Simple Memory": {
        "ai_memory": [
          [
            {
              "node": "AI Agent",
              "type": "ai_memory",
              "index": 0
            }
          ]
        ]
      },
      "Tool: Daftar Model AI": {
        "ai_tool": [
          [
            {
              "node": "AI Agent",
              "type": "ai_tool",
              "index": 0
            }
          ]
        ]
      },
      "AI Agent": {
        "main": [
          [
            {
              "node": "Respond to Webhook",
              "type": "main",
              "index": 0
            }
          ]
        ]
      }
    },
    "settings": {
      "executionOrder": "v1",
      "saveManualExecutions": true,
      "callerPolicy": "workflowsFromSameOwner"
    },
    "staticData": null,
    "meta": null,
    "nodeGroups": [],
    "pinData": {},
    "versionId": "e88f6f18-747a-4346-a099-6a15934f9a40",
    "activeVersionId": "e88f6f18-747a-4346-a099-6a15934f9a40",
    "versionCounter": 5,
    "triggerCount": 1,
    "sourceWorkflowId": null,
    "tags": [],
    "shared": [
      {
        "updatedAt": "2026-10-21T21:29:38.313Z",
        "createdAt": "2026-10-21T21:29:38.313Z",
        "role": "workflow:owner",
        "workflowId": "RBeftVKCMM3sApGD",
        "projectId": "O5dS2R3vFvvVOZZK",
        "project": {
          "updatedAt": "2026-10-23T06:20:42.590Z",
          "createdAt": "2026-10-21T04:53:49.401Z",
          "id": "O5dS2R3vFvvVOZZK",
          "name": "Mohammad Khoiruman <arifkhoiruman2@gmail.com>",
          "type": "personal",
          "icon": null,
          "description": null,
          "customTelemetryTags": [],
          "creatorId": "77323fa2-5a74-405e-999c-961acc6ca1ba"
        }
      }
    ],
    "versionMetadata": {
      "name": null,
      "description": null
    }
  }
]