{
  "id": "oaSeoDemo00000002",
  "name": "Ops Automators - Pipeline digest",
  "nodes": [
    {
      "id": "manual",
      "name": "Run sample",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        0,
        260
      ]
    },
    {
      "id": "sample",
      "name": "Sample records",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [\n  {\n    \"windowStart\": \"2026-09-06T00:00:00Z\",\n    \"windowEnd\": \"2026-09-07T00:00:00Z\",\n    \"opportunities\": [\n      {\n        \"id\": \"D-104\",\n        \"stage\": \"Closed Won\",\n        \"currency\": \"USD\",\n        \"amountMinor\": 250000,\n        \"updatedAt\": \"2026-09-06T12:00:00Z\"\n      },\n      {\n        \"id\": \"D-105\",\n        \"stage\": \"Proposal\",\n        \"currency\": \"USD\",\n        \"amountMinor\": 175000,\n        \"updatedAt\": \"2026-09-06T16:00:00Z\"\n      },\n      {\n        \"id\": \"D-106\",\n        \"stage\": \"Proposal\",\n        \"currency\": \"EUR\",\n        \"amountMinor\": 90000,\n        \"updatedAt\": \"2026-09-06T18:00:00Z\"\n      }\n    ]\n  }\n].map(json => ({ json }));"
      },
      "position": [
        240,
        260
      ]
    },
    {
      "id": "logic",
      "name": "Build digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return $input.all().map(({ json: input }) => {\n  const start = Date.parse(input.windowStart), end = Date.parse(input.windowEnd);\n  if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) throw new Error('Provide a valid start/end UTC interval.');\n  if (!Array.isArray(input.opportunities)) throw new Error('opportunities must be an array.');\n  const latest = new Map(), invalidIds = [], groups = new Map();\n  let outsideWindow = 0;\n  for (const row of input.opportunities) {\n    if (!row || typeof row.id !== 'string' || !row.id.trim() || typeof row.stage !== 'string' || !row.stage.trim() || !['USD','EUR','GBP'].includes(row.currency) || !Number.isSafeInteger(row.amountMinor) || row.amountMinor < 0 || !Number.isFinite(Date.parse(row.updatedAt))) { invalidIds.push(String(row?.id || 'missing ID')); continue; }\n    const updated = Date.parse(row.updatedAt);\n    if (updated < start || updated >= end) { outsideWindow++; continue; }\n    if (!latest.has(row.id) || updated > Date.parse(latest.get(row.id).updatedAt)) latest.set(row.id, row);\n  }\n  for (const row of latest.values()) {\n    const key = JSON.stringify([row.stage, row.currency]);\n    const group = groups.get(key) || { stage: row.stage, currency: row.currency, count: 0, amountMinor: 0 };\n    group.count++; group.amountMinor += row.amountMinor;\n    if (!Number.isSafeInteger(group.amountMinor)) throw new Error('Amount exceeds safe integer range.');\n    groups.set(key, group);\n  }\n  const totals = [...groups.values()].sort((a,b) => a.stage.localeCompare(b.stage) || a.currency.localeCompare(b.currency));\n  const text = totals.length ? totals.map(g => g.stage + ': ' + g.count + (g.count === 1 ? ' deal; ' : ' deals; ') + g.currency + ' ' + (g.amountMinor / 100).toFixed(2)).join('\\n') : 'No eligible opportunity updates in this interval.';\n  return { json: { windowStart: input.windowStart, windowEnd: input.windowEnd, included: latest.size, invalidIds, outsideWindow, totals, text, note: 'Updated opportunity snapshots, not a stage-change history or revenue-recognition report.' } };\n});"
      },
      "position": [
        500,
        260
      ]
    },
    {
      "id": "instructions",
      "name": "Read before connecting systems",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Ops Automators - Pipeline digest\n\nRun the fixed sample to inspect a daily digest. Replace Sample records with your CRM query, keeping the envelope and fields shown. The interval is UTC and end-exclusive. The template groups totals by stage and currency; it does not send messages or fetch your CRM.\n\nSetup: https://www.opsautomators.com/blog/n8n-workflow-templates\n\nSynthetic demo data. No credentials required. MIT licensed.",
        "height": 260,
        "width": 740
      },
      "position": [
        0,
        -70
      ]
    }
  ],
  "connections": {
    "Run sample": {
      "main": [
        [
          {
            "node": "Sample records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample records": {
      "main": [
        [
          {
            "node": "Build digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "tags": []
}
