{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        96,
        992
      ],
      "id": "1cd6bd1c-fa7b-43b2-b6ec-b9edd84e87f7",
      "name": "Trace Sync Trigger"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH last_seen AS (\n  SELECT COALESCE(MAX(execution_id::bigint), 0) AS last_id\n  FROM public.executions\n  WHERE instance_id = '{{ $json.INSTANCE_ID }}'\n),\nrefresh AS (\n  SELECT execution_id::bigint AS id\n  FROM public.executions\n  WHERE instance_id = 'prod'\n    AND (\n      finished = false\n      OR status IN ('running','waiting')\n      OR stopped_at IS NULL\n    )\n  ORDER BY execution_id::bigint DESC\n  LIMIT 200\n)\nSELECT\n  (SELECT last_id FROM last_seen) AS last_id,\n  COALESCE(json_agg(id), '[]'::json) AS refresh_ids\nFROM refresh;\n",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        624,
        1216
      ],
      "id": "4017e169-52ed-4464-bfda-1816fe6657fc",
      "name": "Load Trace Checkpoint (last_id + refresh)"
    },
    {
      "parameters": {
        "jsCode": "const row = $input.first().json;\n\nconst lastId = Number(row.last_id ?? 0);\nconst refreshIds = Array.isArray(row.refresh_ids) ? row.refresh_ids.map(Number).filter(Number.isFinite) : [];\n\nreturn [{\n  json: {\n    lastId,\n    refreshIds,\n    maxBytes: 5 * 1024 * 1024,\n    refreshLimit: 200,\n    newLimit: 200,\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        848,
        1216
      ],
      "id": "95842e49-cf6c-4aff-87a1-4d8eb7c0c135",
      "name": "Normalize Checkpoint Params"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH params AS (\n  SELECT\n    {{ Number($json.lastId) }}::bigint AS last_id,\n    ARRAY[ {{ ($json.refreshIds || []).join(',') || 'NULL' }} ]::bigint[] AS refresh_ids,\n    {{ Number($json.newLimit ?? 200) }}::int AS new_limit\n),\n\nnew_rows AS (\n  SELECT * FROM (\n    SELECT\n      e.id AS id,\n      e.id::text AS \"executionId\",\n      e.\"workflowId\"::text AS \"workflowId\",\n      e.status,\n      e.finished,\n      e.mode,\n      e.\"startedAt\",\n      e.\"stoppedAt\",\n      e.\"waitTill\",\n      e.\"retryOf\",\n      e.\"retrySuccessId\",\n      COALESCE(octet_length(ed.data), 0) AS \"dataBytesLen\",\n      (ed.data IS NOT NULL) AS \"hasData\",\n      0 AS priority\n    FROM execution_entity e\n    LEFT JOIN execution_data ed ON ed.\"executionId\" = e.id\n    CROSS JOIN params p\n    WHERE p.last_id > 0\n      AND e.id > p.last_id\n    ORDER BY e.id\n    LIMIT (SELECT new_limit FROM params)\n  ) inc\n\n  UNION ALL\n\n  SELECT * FROM (\n    SELECT\n      e.id AS id,\n      e.id::text AS \"executionId\",\n      e.\"workflowId\"::text AS \"workflowId\",\n      e.status,\n      e.finished,\n      e.mode,\n      e.\"startedAt\",\n      e.\"stoppedAt\",\n      e.\"waitTill\",\n      e.\"retryOf\",\n      e.\"retrySuccessId\",\n      COALESCE(octet_length(ed.data), 0) AS \"dataBytesLen\",\n      (ed.data IS NOT NULL) AS \"hasData\",\n      0 AS priority\n    FROM execution_entity e\n    LEFT JOIN execution_data ed ON ed.\"executionId\" = e.id\n    CROSS JOIN params p\n    WHERE p.last_id = 0\n    ORDER BY e.id DESC\n    LIMIT (SELECT new_limit FROM params)\n  ) boot\n),\n\nrefresh_rows AS (\n  SELECT\n    e.id AS id,\n    e.id::text AS \"executionId\",\n    e.\"workflowId\"::text AS \"workflowId\",\n    e.status,\n    e.finished,\n    e.mode,\n    e.\"startedAt\",\n    e.\"stoppedAt\",\n    e.\"waitTill\",\n    e.\"retryOf\",\n    e.\"retrySuccessId\",\n    COALESCE(octet_length(ed.data), 0) AS \"dataBytesLen\",\n    (ed.data IS NOT NULL) AS \"hasData\",\n    1 AS priority\n  FROM execution_entity e\n  LEFT JOIN execution_data ed ON ed.\"executionId\" = e.id\n  CROSS JOIN params p\n  WHERE p.refresh_ids IS NOT NULL\n    AND e.id = ANY(p.refresh_ids)\n)\n\nSELECT DISTINCT ON (id) *\nFROM (\n  SELECT * FROM new_rows\n  UNION ALL\n  SELECT * FROM refresh_rows\n) x\nORDER BY id, priority DESC;\n",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1072,
        1216
      ],
      "id": "1aac4ae2-ba8e-47aa-b026-f2a282d7acdf",
      "name": "Fetch Execution Headers from n8n"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "7e6f235e-a198-4a33-9b80-83692abe179d",
              "leftValue": "={{ $json.workflowId }}",
              "rightValue": "={{$workflow.id}}",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1296,
        1216
      ],
      "id": "1a70c9bc-9be2-415a-8fc6-12f3638a2321",
      "name": "Filter Specific Workflow (debug)"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "b6b9bbc8-6743-4514-94bc-a49de1896ab7",
              "leftValue": "={{ $json.dataBytesLen }}",
              "rightValue": "={{Number( 5 * 1024 * 1024) }}",
              "operator": {
                "type": "number",
                "operation": "lt"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1520,
        1120
      ],
      "id": "a2412198-4deb-44dc-8762-265a39f82d63",
      "name": "Is Execution Data Small?"
    },
    {
      "parameters": {
        "jsCode": "const ids = $input.all()\n  .map(i => Number(i.json.id))\n  .filter(Number.isFinite);\n\nif (!ids.length) return [];\n\nreturn [{ json: { executionIds: ids, maxBytes: 5 * 1024 * 1024 } }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1744,
        816
      ],
      "id": "724224d8-0e82-4321-ab60-7daa40a31277",
      "name": "Collect Execution IDs (small only)"
    },
    {
      "parameters": {
        "jsCode": "const ids = $json.executionIds || [];\nconst maxBytes = $json.maxBytes ?? 5 * 1024 * 1024;\n\nconst chunkSize = 20;\nconst out = [];\n\nfor (let i = 0; i < ids.length; i += chunkSize) {\n  out.push({ json: { executionIds: ids.slice(i, i + chunkSize), maxBytes } });\n}\n\nreturn out;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1920,
        816
      ],
      "id": "498d464f-2f85-4c7b-9619-834d1571b496",
      "name": "Chunk Execution IDs"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  e.id::text AS \"executionId\",\n  e.\"workflowId\"::text AS \"workflowId\",\n  e.status,\n  e.finished,\n  e.mode,\n  e.\"startedAt\",\n  e.\"stoppedAt\",\n  e.\"waitTill\",\n  e.\"retryOf\",\n  e.\"retrySuccessId\",\n  ed.data,\n  w.nodes AS \"workflowNodes\"\nFROM execution_entity e\nJOIN execution_data ed ON ed.\"executionId\" = e.id\nLEFT JOIN workflow_entity w ON w.id = e.\"workflowId\"\nWHERE e.id = ANY(ARRAY[ {{ $json.executionIds.join(',') }} ]::int[])\n  AND octet_length(ed.data) <= {{ Number($json.maxBytes) }}\nORDER BY e.id;\n",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2128,
        816
      ],
      "id": "50f2fb7e-c7e8-48f0-a0c6-4326547de2fd",
      "name": "Load Execution Data + Workflow Nodes",
      "executeOnce": false
    },
    {
      "parameters": {
        "jsCode": "const TIMEZONE = \"UTC\";\nconst INSTANCE_ID = $('Set Instance Context').first().json.INSTANCE_ID;\nfunction isIndexString(s) {\n  return typeof s === \"string\" && /^\\d+$/.test(s);\n}\n\nfunction resolve(val, dict) {\n  if (val === null || val === undefined) return null;\n  if (isIndexString(val)) {\n    const idx = Number(val);\n    return dict[idx] ?? val;\n  }\n  return val;\n}\n\nfunction deepResolve(x, dict) {\n  if (Array.isArray(x)) return x.map(v => deepResolve(v, dict));\n  if (x && typeof x === \"object\") {\n    const o = {};\n    for (const [k, v] of Object.entries(x)) {\n      o[k] = deepResolve(resolve(v, dict), dict);\n    }\n    return o;\n  }\n  return resolve(x, dict);\n}\n\nfunction formatDateTime(dt) {\n  if (!dt) return null;\n  const d = new Date(dt);\n  if (isNaN(d.getTime())) return null;\n  return d.toISOString().replace(\"T\", \" \").replace(\"Z\", \"\");\n}\n\nfunction countItemsOutFromRunResolved(runResolved) {\n  const main = runResolved?.data?.main || [];\n  return main.reduce((sum, outputArr) => sum + (outputArr?.length || 0), 0);\n}\n\nfunction buildNodeTypeMap(row, dict) {\n\n  try {\n    const wfNodes =\n      typeof row.workflowNodes === \"string\"\n        ? JSON.parse(row.workflowNodes)\n        : row.workflowNodes;\n\n    if (Array.isArray(wfNodes)) {\n      const map = {};\n      for (const n of wfNodes) {\n        if (n?.name) map[n.name] = n.type || null;\n      }\n      if (Object.keys(map).length) return map;\n    }\n  } catch (_) {}\n\n  for (let i = 0; i < dict.length; i++) {\n    const entry = dict[i];\n    if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) continue;\n\n    const resolved = deepResolve(entry, dict);\n    const nodes = resolved?.nodes;\n\n    if (Array.isArray(nodes) && nodes.some(n => n?.name && n?.type)) {\n      const map = {};\n      for (const n of nodes) {\n        if (n?.name) map[n.name] = n.type || null;\n      }\n      if (Object.keys(map).length) return map;\n    }\n  }\n\n  return {};\n}\n\nconst out = [];\n\nfor (const item of $input.all()) {\n  const row = item.json || {};\n\n  let dict;\n  try {\n    dict = JSON.parse(row.data);\n  } catch (e) {\n    out.push({\n      json: {\n        error: \"\u062d\u0642\u0644 data \u0644\u064a\u0633 JSON \u0635\u0627\u0644\u062d\",\n        executionId: row.executionId ?? null,\n        details: e.message,\n      }\n    });\n    continue;\n  }\n\n  const resultData = deepResolve(dict[2] || {}, dict);\n  const runData = resultData?.runData || {};\n  const lastNodeExecuted = resultData?.lastNodeExecuted ?? null;\n  const nodeTypeByName = buildNodeTypeMap(row, dict);\n  const startedAtIso = row.startedAt ?? null;\n  const stoppedAtIso = row.stoppedAt ?? null;\n  const durationMs =\n    startedAtIso && stoppedAtIso\n      ? (new Date(stoppedAtIso).getTime() - new Date(startedAtIso).getTime())\n      : null;\n\n  out.push({\n    json: {\n      rowType: \"execution\",\n      instanceId: INSTANCE_ID,\n      executionId: row.executionId != null ? String(row.executionId) : null,\n      workflowId: row.workflowId != null ? String(row.workflowId) : null,\n\n      status: row.status ?? null,\n      finished: row.finished ?? null,\n      mode: row.mode ?? null,\n\n      startedAt: startedAtIso,\n      stoppedAt: stoppedAtIso,\n      startedAtFmt: formatDateTime(startedAtIso),\n      stoppedAtFmt: formatDateTime(stoppedAtIso),\n      durationMs,\n\n      waitTill: row.waitTill ?? null,\n      retryOf: row.retryOf ?? null,\n      retrySuccessId: row.retrySuccessId ?? null,\n\n      lastNodeExecuted: resolve(lastNodeExecuted, dict),\n      nodeNamesExecuted: Object.keys(runData),\n      nodesCount: Object.keys(runData).length,\n\n      timeFormatTz: TIMEZONE,\n    }\n  });\n\n  for (const [nodeName, runs] of Object.entries(runData)) {\n    const runsArr = Array.isArray(runs) ? runs : [];\n    const runsResolved = runsArr.map(r => deepResolve(r, dict));\n    const itemsOutPerRun = runsResolved.map(countItemsOutFromRunResolved);\n    const itemsOutTotalAllRuns = itemsOutPerRun.reduce((a, b) => a + b, 0);\n\n    runsResolved.forEach((runResolved, runIndex) => {\n      out.push({\n        json: {\n          rowType: \"execution_node\",\n          instanceId: INSTANCE_ID,\n          executionId: row.executionId != null ? String(row.executionId) : null,\n          workflowId: row.workflowId != null ? String(row.workflowId) : null,\n\n          nodeName,\n          nodeType: nodeTypeByName[nodeName] || null,\n\n          runIndex,\n          runsCount: runsResolved.length,\n          isLastRun: runIndex === runsResolved.length - 1,\n          executionStatus: runResolved?.executionStatus ?? null,\n          executionTimeMs: runResolved?.executionTime ?? null,\n\n          startTimeMs: runResolved?.startTime ?? null,\n          startTimeIso: runResolved?.startTime != null ? new Date(runResolved.startTime).toISOString() : null,\n          startTimeFmt: runResolved?.startTime != null ? formatDateTime(runResolved.startTime) : null,\n\n          itemsOutCount: itemsOutPerRun[runIndex] ?? 0,\n          itemsOutTotalAllRuns,\n\n          lastNodeExecuted: resolve(lastNodeExecuted, dict),\n          timeFormatTz: TIMEZONE,\n        }\n      });\n    });\n  }\n}\n\nreturn out;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2304,
        816
      ],
      "id": "1b83fa2c-7a6e-42bd-bded-8b396fbe8d5c",
      "name": "Parse runData \u2192 Execution + Node Rows"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 3
          },
          "conditions": [
            {
              "id": "c61ad465-9e9e-460c-915b-b679e330326f",
              "leftValue": "={{$json.rowType}}",
              "rightValue": "execution",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        2544,
        816
      ],
      "id": "81cbbf72-98d7-4ba7-955c-39bd60b473c4",
      "name": "Route RowType (execution vs node)"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH j AS (SELECT $1::jsonb AS j),\n\npayload AS (\n  SELECT\n    j->>'instanceId'                                AS instance_id,\n    j->>'executionId'                               AS execution_id,\n    NULLIF(j->>'workflowId','')                     AS workflow_id,\n    NULLIF(j->>'status','')                         AS status_raw,\n    NULLIF(j->>'mode','')                           AS mode,\n    NULLIF(j->>'startedAt','null')::timestamptz     AS started_at,\n    NULLIF(j->>'stoppedAt','null')::timestamptz     AS stopped_at_in,\n    NULLIF(j->>'waitTill','null')::timestamptz      AS wait_till,\n    NULLIF(j->>'retryOf','null')                    AS retry_of,\n    NULLIF(j->>'retrySuccessId','null')             AS retry_success_id,\n    NULLIF(j->>'lastNodeExecuted','null')           AS last_node_executed,\n    COALESCE(j->'nodeNamesExecuted', '[]'::jsonb)   AS node_names_executed,\n    COALESCE(NULLIF(j->>'nodesCount','null')::int, 0) AS nodes_count,\n    NULLIF(j->>'durationMs','null')::bigint         AS duration_ms_in,\n    COALESCE(NULLIF(j->>'finished','null')::boolean, false) AS finished_in\n  FROM j\n),\n\nnorm AS (\n  SELECT\n    *,\n    (status_raw IN ('success','error','failed','canceled','crashed')) AS is_final,\n\n    CASE\n      WHEN status_raw IN ('success','error','failed','canceled','crashed') THEN true\n      ELSE finished_in\n    END AS finished_norm,\n\n    CASE\n      WHEN status_raw IN ('success','error','failed','canceled','crashed')\n        THEN COALESCE(stopped_at_in, started_at, now())\n      ELSE COALESCE(stopped_at_in, started_at, now())\n    END AS stopped_at_norm,\n\n    COALESCE(duration_ms_in, 0) AS duration_ms_norm\n  FROM payload\n)\n\nINSERT INTO public.executions\n(\n  instance_id,\n  execution_id,\n  workflow_id,\n  status,\n  finished,\n  mode,\n  started_at,\n  stopped_at,\n  duration_ms,\n  wait_till,\n  retry_of,\n  retry_success_id,\n  last_node_executed,\n  node_names_executed,\n  nodes_count\n)\nSELECT\n  instance_id,\n  execution_id,\n  workflow_id,\n  status_raw,\n  finished_norm,\n  mode,\n  started_at,\n  stopped_at_norm,\n  duration_ms_norm,\n  wait_till,\n  retry_of,\n  retry_success_id,\n  last_node_executed,\n  node_names_executed,\n  nodes_count\nFROM norm\n\nON CONFLICT (instance_id, execution_id)\nDO UPDATE SET\n\n  status = CASE\n    WHEN executions.status IN ('success','error','failed','canceled','crashed')\n         AND EXCLUDED.status IN ('running','waiting')\n      THEN executions.status\n    ELSE EXCLUDED.status\n  END,\n\n  finished = CASE\n    WHEN executions.status IN ('success','error','failed','canceled','crashed') THEN true\n    WHEN EXCLUDED.status IN ('success','error','failed','canceled','crashed') THEN true\n    ELSE EXCLUDED.finished\n  END,\n\n  stopped_at = CASE\n    WHEN EXCLUDED.status IN ('success','error','failed','canceled','crashed')\n      THEN COALESCE(EXCLUDED.stopped_at, executions.stopped_at, now())\n    ELSE COALESCE(executions.stopped_at, EXCLUDED.stopped_at, now())\n  END,\n\n  workflow_id = COALESCE(EXCLUDED.workflow_id, executions.workflow_id),\n  mode = COALESCE(EXCLUDED.mode, executions.mode),\n  started_at = COALESCE(EXCLUDED.started_at, executions.started_at),\n  duration_ms = COALESCE(EXCLUDED.duration_ms, executions.duration_ms, 0),\n  wait_till = COALESCE(EXCLUDED.wait_till, executions.wait_till),\n  retry_of = COALESCE(EXCLUDED.retry_of, executions.retry_of),\n  retry_success_id = COALESCE(EXCLUDED.retry_success_id, executions.retry_success_id),\n  last_node_executed = COALESCE(EXCLUDED.last_node_executed, executions.last_node_executed),\n  node_names_executed = COALESCE(EXCLUDED.node_names_executed, executions.node_names_executed),\n  nodes_count = COALESCE(EXCLUDED.nodes_count, executions.nodes_count);\n",
        "options": {
          "queryReplacement": "={{ JSON.stringify($json) }}\n"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2784,
        800
      ],
      "id": "2341ac93-68cd-4756-9ad9-befe66bf8fbe",
      "name": "Upsert Execution Summary"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH j AS (SELECT $1::jsonb AS j),\n\nupd AS (\n  UPDATE public.execution_nodes n\n  SET\n    workflow_id = COALESCE(NULLIF(j.j->>'workflowId','null'), n.workflow_id),\n    node_type   = COALESCE(NULLIF(j.j->>'nodeType','null'), n.node_type),\n    runs_count  = COALESCE(NULLIF(j.j->>'runsCount','null')::int, n.runs_count),\n\n    is_last_run = COALESCE(\n      CASE WHEN jsonb_typeof(j.j->'isLastRun') = 'boolean' THEN (j.j->>'isLastRun')::boolean END,\n      NULLIF(j.j->>'isLastRun','null')::boolean,\n      n.is_last_run\n    ),\n\n    execution_status  = COALESCE(NULLIF(j.j->>'executionStatus','null'), n.execution_status),\n    execution_time_ms = COALESCE(NULLIF(j.j->>'executionTimeMs','null')::bigint, n.execution_time_ms),\n\n    start_time_ms = COALESCE(NULLIF(j.j->>'startTimeMs','null')::bigint, n.start_time_ms),\n    start_time    = COALESCE(\n      NULLIF(j.j->>'startTimeIso','null')::timestamptz,\n      CASE\n        WHEN NULLIF(j.j->>'startTimeMs','null') IS NOT NULL\n          THEN to_timestamp((j.j->>'startTimeMs')::double precision / 1000.0)::timestamptz\n      END,\n      n.start_time\n    ),\n\n    items_out_count = COALESCE(NULLIF(j.j->>'itemsOutCount','null')::int, n.items_out_count),\n    items_out_total_all_runs = COALESCE(NULLIF(j.j->>'itemsOutTotalAllRuns','null')::int, n.items_out_total_all_runs)\n\n  FROM j\n  WHERE n.instance_id  = j.j->>'instanceId'\n    AND n.execution_id = j.j->>'executionId'\n    AND n.node_name    = j.j->>'nodeName'\n    AND n.run_index    = COALESCE(NULLIF(j.j->>'runIndex','')::int, 0)\n  RETURNING 1\n)\n\nINSERT INTO public.execution_nodes\n(\n  instance_id,\n  execution_id,\n  workflow_id,\n  node_name,\n  node_type,\n  run_index,\n  runs_count,\n  is_last_run,\n  execution_status,\n  execution_time_ms,\n  start_time_ms,\n  start_time,\n  items_out_count,\n  items_out_total_all_runs\n)\nSELECT\n  j.j->>'instanceId',\n  j.j->>'executionId',\n  NULLIF(j.j->>'workflowId','null'),\n  j.j->>'nodeName',\n  NULLIF(j.j->>'nodeType','null'),\n  COALESCE(NULLIF(j.j->>'runIndex','')::int, 0),\n  COALESCE(NULLIF(j.j->>'runsCount','null')::int, 1),\n\n  COALESCE(\n    CASE WHEN jsonb_typeof(j.j->'isLastRun') = 'boolean' THEN (j.j->>'isLastRun')::boolean END,\n    false\n  ),\n\n  COALESCE(NULLIF(j.j->>'executionStatus','null'), 'unknown'),\n  COALESCE(NULLIF(j.j->>'executionTimeMs','null')::bigint, 0),\n\n  NULLIF(j.j->>'startTimeMs','null')::bigint,\n  COALESCE(\n    NULLIF(j.j->>'startTimeIso','null')::timestamptz,\n    CASE\n      WHEN NULLIF(j.j->>'startTimeMs','null') IS NOT NULL\n        THEN to_timestamp((j.j->>'startTimeMs')::double precision / 1000.0)::timestamptz\n    END\n  ),\n\n  COALESCE(NULLIF(j.j->>'itemsOutCount','null')::int, 0),\n  COALESCE(NULLIF(j.j->>'itemsOutTotalAllRuns','null')::int, 0)\n\nFROM j\nWHERE NOT EXISTS (SELECT 1 FROM upd)\n  AND NULLIF(j.j->>'workflowId','null') IS NOT NULL\n  AND NULLIF(j.j->>'nodeType','null') IS NOT NULL;\n",
        "options": {
          "queryReplacement": "={{ JSON.stringify($json) }}\n"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        2784,
        1024
      ],
      "id": "b7d55a3f-0572-486d-8427-8a71780ecfeb",
      "name": "Upsert Execution Nodes"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH j AS (SELECT $1::jsonb AS j),\nrt AS (SELECT COALESCE(j.j->>'rowType','') AS row_type FROM j)\n\n, exec_payload AS (\n  SELECT\n    j.j->>'instanceId'                                AS instance_id,\n    j.j->>'executionId'                               AS execution_id,\n    NULLIF(j.j->>'workflowId','')                     AS workflow_id,\n    NULLIF(j.j->>'status','')                         AS status_raw,\n    NULLIF(j.j->>'mode','')                           AS mode,\n    NULLIF(j.j->>'startedAt','null')::timestamptz     AS started_at,\n    NULLIF(j.j->>'stoppedAt','null')::timestamptz     AS stopped_at_in,\n    NULLIF(j.j->>'waitTill','null')::timestamptz      AS wait_till,\n    NULLIF(j.j->>'retryOf','null')                    AS retry_of,\n    NULLIF(j.j->>'retrySuccessId','null')             AS retry_success_id,\n    NULLIF(j.j->>'lastNodeExecuted','null')           AS last_node_executed,\n    COALESCE(j.j->'nodeNamesExecuted', '[]'::jsonb)   AS node_names_executed,\n    COALESCE(NULLIF(j.j->>'nodesCount','null')::int, 0) AS nodes_count,\n    NULLIF(j.j->>'durationMs','null')::bigint         AS duration_ms_in,\n    COALESCE(NULLIF(j.j->>'finished','null')::boolean, false) AS finished_in\n  FROM j, rt\n  WHERE rt.row_type = 'execution'\n),\nexec_norm AS (\n  SELECT\n    *,\n    CASE\n      WHEN status_raw IN ('success','error','failed','canceled','crashed') THEN true\n      ELSE finished_in\n    END AS finished_norm,\n    CASE\n      WHEN status_raw IN ('success','error','failed','canceled','crashed')\n        THEN COALESCE(stopped_at_in, started_at, now())\n      ELSE COALESCE(stopped_at_in, started_at, now())\n    END AS stopped_at_norm,\n    COALESCE(duration_ms_in, 0) AS duration_ms_norm\n  FROM exec_payload\n),\nexec_upsert AS (\n  INSERT INTO public.executions\n  (\n    instance_id,\n    execution_id,\n    workflow_id,\n    status,\n    finished,\n    mode,\n    started_at,\n    stopped_at,\n    duration_ms,\n    wait_till,\n    retry_of,\n    retry_success_id,\n    last_node_executed,\n    node_names_executed,\n    nodes_count\n  )\n  SELECT\n    instance_id,\n    execution_id,\n    workflow_id,\n    status_raw,\n    finished_norm,\n    mode,\n    started_at,\n    stopped_at_norm,\n    duration_ms_norm,\n    wait_till,\n    retry_of,\n    retry_success_id,\n    last_node_executed,\n    node_names_executed,\n    nodes_count\n  FROM exec_norm\n  ON CONFLICT (instance_id, execution_id)\n  DO UPDATE SET\n    status = CASE\n      WHEN executions.status IN ('success','error','failed','canceled','crashed')\n           AND EXCLUDED.status IN ('running','waiting')\n        THEN executions.status\n      ELSE EXCLUDED.status\n    END,\n    finished = CASE\n      WHEN executions.status IN ('success','error','failed','canceled','crashed') THEN true\n      WHEN EXCLUDED.status IN ('success','error','failed','canceled','crashed') THEN true\n      ELSE EXCLUDED.finished\n    END,\n    stopped_at = CASE\n      WHEN EXCLUDED.status IN ('success','error','failed','canceled','crashed')\n        THEN COALESCE(EXCLUDED.stopped_at, executions.stopped_at, now())\n      ELSE COALESCE(executions.stopped_at, EXCLUDED.stopped_at, now())\n    END,\n    workflow_id = COALESCE(EXCLUDED.workflow_id, executions.workflow_id),\n    mode = COALESCE(EXCLUDED.mode, executions.mode),\n    started_at = COALESCE(EXCLUDED.started_at, executions.started_at),\n    duration_ms = COALESCE(EXCLUDED.duration_ms, executions.duration_ms, 0),\n    wait_till = COALESCE(EXCLUDED.wait_till, executions.wait_till),\n    retry_of = COALESCE(EXCLUDED.retry_of, executions.retry_of),\n    retry_success_id = COALESCE(EXCLUDED.retry_success_id, executions.retry_success_id),\n    last_node_executed = COALESCE(EXCLUDED.last_node_executed, executions.last_node_executed),\n    node_names_executed = COALESCE(EXCLUDED.node_names_executed, executions.node_names_executed),\n    nodes_count = COALESCE(EXCLUDED.nodes_count, executions.nodes_count)\n  RETURNING 'execution'::text AS inserted_type\n)\n\n, node_upd AS (\n  UPDATE public.execution_nodes n\n  SET\n    workflow_id = COALESCE(NULLIF(j.j->>'workflowId','null'), n.workflow_id),\n    node_type   = COALESCE(NULLIF(j.j->>'nodeType','null'), n.node_type),\n    runs_count  = COALESCE(NULLIF(j.j->>'runsCount','null')::int, n.runs_count),\n    is_last_run = COALESCE(\n      CASE WHEN jsonb_typeof(j.j->'isLastRun')='boolean' THEN (j.j->>'isLastRun')::boolean END,\n      NULLIF(j.j->>'isLastRun','null')::boolean,\n      n.is_last_run\n    ),\n    execution_status  = COALESCE(NULLIF(j.j->>'executionStatus','null'), n.execution_status),\n    execution_time_ms = COALESCE(NULLIF(j.j->>'executionTimeMs','null')::bigint, n.execution_time_ms),\n    start_time_ms = COALESCE(NULLIF(j.j->>'startTimeMs','null')::bigint, n.start_time_ms),\n    start_time = COALESCE(\n      NULLIF(j.j->>'startTimeIso','null')::timestamptz,\n      CASE\n        WHEN NULLIF(j.j->>'startTimeMs','null') IS NOT NULL\n          THEN to_timestamp((j.j->>'startTimeMs')::double precision / 1000.0)::timestamptz\n      END,\n      n.start_time\n    ),\n    items_out_count = COALESCE(NULLIF(j.j->>'itemsOutCount','null')::int, n.items_out_count),\n    items_out_total_all_runs = COALESCE(NULLIF(j.j->>'itemsOutTotalAllRuns','null')::int, n.items_out_total_all_runs)\n  FROM j, rt\n  WHERE rt.row_type = 'execution_node'\n    AND n.instance_id  = j.j->>'instanceId'\n    AND n.execution_id = j.j->>'executionId'\n    AND n.node_name    = j.j->>'nodeName'\n    AND n.run_index    = COALESCE(NULLIF(j.j->>'runIndex','')::int, 0)\n  RETURNING 1\n),\nnode_ins AS (\n  INSERT INTO public.execution_nodes\n  (\n    instance_id,\n    execution_id,\n    workflow_id,\n    node_name,\n    node_type,\n    run_index,\n    runs_count,\n    is_last_run,\n    execution_status,\n    execution_time_ms,\n    start_time_ms,\n    start_time,\n    items_out_count,\n    items_out_total_all_runs\n  )\n  SELECT\n    j.j->>'instanceId',\n    j.j->>'executionId',\n    NULLIF(j.j->>'workflowId','null'),\n    j.j->>'nodeName',\n    NULLIF(j.j->>'nodeType','null'),\n    COALESCE(NULLIF(j.j->>'runIndex','')::int, 0),\n    COALESCE(NULLIF(j.j->>'runsCount','null')::int, 1),\n    COALESCE(\n      CASE WHEN jsonb_typeof(j.j->'isLastRun')='boolean' THEN (j.j->>'isLastRun')::boolean END,\n      false\n    ),\n    COALESCE(NULLIF(j.j->>'executionStatus','null'),'unknown'),\n    COALESCE(NULLIF(j.j->>'executionTimeMs','null')::bigint, 0),\n    NULLIF(j.j->>'startTimeMs','null')::bigint,\n    COALESCE(\n      NULLIF(j.j->>'startTimeIso','null')::timestamptz,\n      CASE\n        WHEN NULLIF(j.j->>'startTimeMs','null') IS NOT NULL\n          THEN to_timestamp((j.j->>'startTimeMs')::double precision / 1000.0)::timestamptz\n      END\n    ),\n    COALESCE(NULLIF(j.j->>'itemsOutCount','null')::int, 0),\n    COALESCE(NULLIF(j.j->>'itemsOutTotalAllRuns','null')::int, 0)\n  FROM j, rt\n  WHERE rt.row_type = 'execution_node'\n    AND NOT EXISTS (SELECT 1 FROM node_upd)\n    AND NULLIF(j.j->>'workflowId','null') IS NOT NULL\n    AND NULLIF(j.j->>'nodeType','null') IS NOT NULL\n  RETURNING 'execution_node'::text AS inserted_type\n)\n\nSELECT\n  COALESCE((SELECT inserted_type FROM exec_upsert LIMIT 1),\n           (SELECT inserted_type FROM node_ins LIMIT 1),\n           'noop') AS result;\n",
        "options": {
          "queryReplacement": "=={{ JSON.stringify($json) }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1936,
        1472
      ],
      "id": "47251abc-ef03-455b-8274-dcf3bef7212a",
      "name": "Generic Row Upsert (execution/node)"
    },
    {
      "parameters": {
        "jsCode": "const INSTANCE_ID = $('Set Instance Context').first().json.INSTANCE_ID;\n\nfunction durationMs(startedAt, stoppedAt) {\n  if (!startedAt || !stoppedAt) return null;\n  const s = new Date(startedAt).getTime();\n  const t = new Date(stoppedAt).getTime();\n  if (!Number.isFinite(s) || !Number.isFinite(t)) return null;\n  return Math.max(0, t - s);\n}\n\nreturn $input.all().map(item => {\n  const r = item.json;\n\n  return {\n    json: {\n      rowType: \"execution\",\n      instanceId: INSTANCE_ID,\n      executionId: r.executionId != null ? String(r.executionId) : null,\n      workflowId: r.workflowId != null ? String(r.workflowId) : null,\n\n      status: r.status ?? null,\n      finished: r.finished ?? null,\n      mode: r.mode ?? null,\n\n      startedAt: r.startedAt ?? null,\n      stoppedAt: r.stoppedAt ?? null,\n      durationMs: durationMs(r.startedAt, r.stoppedAt),\n\n      waitTill: r.waitTill ?? null,\n      retryOf: r.retryOf ?? null,\n      retrySuccessId: r.retrySuccessId ?? null,\n      lastNodeExecuted: null,\n      nodeNamesExecuted: [],\n      nodesCount: 0,\n    }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1744,
        1472
      ],
      "id": "1fa34285-cd3d-4abe-929a-44c66ba8510c",
      "name": "Build Header-Only Execution Row"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  w.id AS \"workflowId\",\n  w.name,\n  w.active,\n  COALESCE(w.\"isArchived\", false) AS \"isArchived\",\n  w.\"createdAt\",\n  w.\"updatedAt\",\n\n  -- tags\n  COALESCE(\n    (\n      SELECT jsonb_agg(t.name ORDER BY t.name)\n      FROM workflows_tags wt\n      JOIN tag_entity t ON t.id = wt.\"tagId\"\n      WHERE wt.\"workflowId\" = w.id\n    ),\n    '[]'::jsonb\n  ) AS \"tags\",\n\n  -- nodesCount\n  jsonb_array_length(COALESCE(w.nodes::jsonb, '[]'::jsonb)) AS \"nodesCount\",\n\n  COALESCE(\n    (\n      SELECT jsonb_agg(DISTINCT (n->>'type'))\n      FROM jsonb_array_elements(COALESCE(w.nodes::jsonb, '[]'::jsonb)) n\n      WHERE (n->>'type') IS NOT NULL\n    ),\n    '[]'::jsonb\n  ) AS \"nodeTypesDistinct\",\n\n  COALESCE(\n    (\n      SELECT jsonb_agg(DISTINCT (n->>'name'))\n      FROM jsonb_array_elements(COALESCE(w.nodes::jsonb, '[]'::jsonb)) n\n      WHERE (n->>'name') IS NOT NULL\n    ),\n    '[]'::jsonb\n  ) AS \"nodeNamesDistinct\"\n\nFROM workflow_entity w\nORDER BY w.\"updatedAt\" DESC\nLIMIT 200;\n",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        608,
        480
      ],
      "id": "2a6cb239-f874-4f03-8d52-4352fe950295",
      "name": "Load Workflows Metadata"
    },
    {
      "parameters": {
        "jsCode": "const INSTANCE_ID =$('Set Instance Context').first().json.INSTANCE_ID\nfunction asArray(v) {\n  if (Array.isArray(v)) return v;\n  if (v == null) return [];\n  if (typeof v === 'string') {\n    try {\n      const parsed = JSON.parse(v);\n      return Array.isArray(parsed) ? parsed : [];\n    } catch {\n      return [];\n    }\n  }\n  return [];\n}\n\nfunction uniqueSorted(arr) {\n  const out = Array.from(new Set(arr.filter(x => x != null && String(x).trim() !== '').map(String)));\n  out.sort((a, b) => a.localeCompare(b));\n  return out;\n}\n\nconst timeFormatTz = 'UTC';\n\n\nconst items = $input.all();\n\nreturn items.map((item) => {\n  const j = item.json || {};\n\n  const tags = uniqueSorted(asArray(j.tags));\n  const nodeTypesDistinct = uniqueSorted(asArray(j.nodeTypesDistinct));\n  const nodeNamesDistinct = uniqueSorted(asArray(j.nodeNamesDistinct));\n\n\n  const nodesCount = Number.isFinite(Number(j.nodesCount)) ? Number(j.nodesCount) : 0;\n\n  return {\n    json: {\n      INSTANCE_ID,\n      rowType: \"workflow_index\",\n      instanceId: j.instanceId ?? \"prod\",\n      workflowId: String(j.workflowId ?? \"\"),\n      name: j.name ?? null,\n      active: !!j.active,\n      isArchived: !!j.isArchived,\n      createdAt: j.createdAt ?? null,\n      updatedAt: j.updatedAt ?? null,\n      tags,\n      nodesCount,\n      nodeTypesDistinct,\n      nodeNamesDistinct,\n      timeFormatTz,\n    }\n  };\n});\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        768,
        480
      ],
      "id": "316a984c-3a14-40d4-a1d6-1c4a938c1f4e",
      "name": "Normalize Workflow Index Rows"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO public.workflows_index\n(\n  instance_id,\n  workflow_id,\n  name,\n  active,\n  is_archived,\n  created_at,\n  updated_at,\n  tags,\n  nodes_count\n)\nVALUES\n(\n  $1,\n  $2,\n  $3,\n  COALESCE($4, false),\n  COALESCE($5, false),\n  $6,\n  $7,\n  $8,\n  $9\n)\nON CONFLICT (workflow_id)\nDO UPDATE SET\n  instance_id  = EXCLUDED.instance_id,\n  name         = EXCLUDED.name,\n  active       = EXCLUDED.active,\n  is_archived  = EXCLUDED.is_archived,\n  created_at   = EXCLUDED.created_at,\n  updated_at   = EXCLUDED.updated_at,\n  tags         = EXCLUDED.tags,\n  nodes_count  = EXCLUDED.nodes_count;\n",
        "options": {
          "queryReplacement": "=`{{$json.instanceId}}`\n`{{$json.workflowId}}`\n`{{$json.name || null}}`\n`{{$json.active ?? null}}`\n`{{$json.isArchived ?? null}}`\n`{{$json.createdAt || null}}`\n`{{$json.updatedAt || null}}`\n`{{$json.tags || []}}`\n`{{$json.nodesCount ?? null}}`"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        944,
        480
      ],
      "id": "299da02e-3a86-4b73-94a9-ca92c744f152",
      "name": "Upsert Workflow Index"
    },
    {
      "parameters": {
        "content": "## n8n-trace Sync \u2014 Overview\n\nThis workflow keeps the n8n-trace database in sync with n8n executions.\n\nIt performs:\n\n- Checkpoint tracking (last processed execution)\n- Incremental ingestion\n- Refresh of running executions\n- Size-aware parsing (avoid huge payloads)\n- Execution + node upserts\n- Workflow metadata indexing\n\nGoal: near-real-time observability without heavy DB load.\n",
        "height": 640,
        "width": 560
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -16,
        -144
      ],
      "typeVersion": 1,
      "id": "1e4b430e-4310-4b58-a25c-1ff885bea7d0",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Checkpoint Strategy\n\nWe store the highest execution ID already ingested.\n\nEach run:\n\n1. Fetch last processed ID from n8n-trace\n2. Identify executions still running\n3. Fetch only new or unfinished executions\n\nThis ensures:\n\n- No duplicates\n- Safe resume\n- Efficient sync\n",
        "height": 672,
        "width": 384,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        768
      ],
      "typeVersion": 1,
      "id": "da7b9d87-9157-4c54-b236-ff3fac3811a6",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Bootstrap Mode\n\nIf the n8n-trace database is empty:\n\nWe start from recent executions instead of full history.\n\nReason:\n\nBackfilling all executions can be expensive and slow.\n\nn8n-trace behaves like a monitoring system \u2014 it observes from \"now\".\n",
        "height": 672,
        "width": 400,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        768
      ],
      "typeVersion": 1,
      "id": "26743888-7f53-4e88-9540-10d7d45b917d",
      "name": "Sticky Note2"
    },
    {
      "parameters": {
        "content": "## Execution Size Guard\n\nExecution payloads larger than ~5MB are not fully parsed.\n\nWhy:\n\nLarge execution_data can be heavy to decode and store.\n\nStrategy:\n\n- Small \u2192 parse runData and store nodes\n- Large \u2192 store execution summary only\n\nThis prevents memory and DB pressure.\n",
        "height": 672,
        "width": 320,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1376,
        768
      ],
      "typeVersion": 1,
      "id": "9cc8fa9b-c9ef-4ff8-9e06-733fff1ada18",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "content": "## runData Parsing\n\nFor small executions we:\n\n- Decode execution_data\n- Extract node runs\n- Compute items out\n- Track last executed node\n- Generate execution + node rows\n\nThis enables deep observability in n8n-trace.\n",
        "height": 560,
        "width": 736,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1712,
        528
      ],
      "typeVersion": 1,
      "id": "475d0f6a-9a5a-4cd0-a2a7-44ada70745fa",
      "name": "Sticky Note4"
    },
    {
      "parameters": {
        "content": "## Idempotent Upserts\n\nAll writes use UPSERT.\n\nBenefits:\n\n- Safe retries\n- No duplicates\n- Handles late status updates\n- Supports running \u2192 finished transitions\n\nWorkflow can run continuously without conflicts.\n",
        "height": 672,
        "width": 544,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2464,
        528
      ],
      "typeVersion": 1,
      "id": "462c66d4-5abb-449d-b218-36d31643d6fe",
      "name": "Sticky Note5"
    },
    {
      "parameters": {
        "content": "## Large Execution Fallback\n\nIf execution is too large:\n\nWe still store:\n\n- Status\n- Timing\n- Workflow ID\n\nBut skip node details.\n\nThis ensures we never miss executions while keeping sync lightweight.\n",
        "height": 672,
        "width": 736,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1712,
        1104
      ],
      "typeVersion": 1,
      "id": "87a3b7a2-6d0b-455c-8b7e-6f9bd02412ba",
      "name": "Sticky Note6"
    },
    {
      "parameters": {
        "content": "## Workflow Index Sync\n\nWe periodically refresh workflow metadata:\n\n- Name\n- Tags\n- Active state\n- Node types\n\nUsed for dashboards and filtering.\n\nIndependent from execution sync.\n",
        "height": 560,
        "width": 592,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        192
      ],
      "typeVersion": 1,
      "id": "bbfd2a7d-d3f7-4b32-b8fd-256e8dcab858",
      "name": "Sticky Note7"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "8ebd10c8-26ee-431b-aa4c-a0fae1dee5e4",
              "name": "INSTANCE_ID",
              "value": "prod",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        368,
        880
      ],
      "id": "e67a4e21-c055-4bfa-ac00-1d971e846047",
      "name": "Set Instance Context"
    },
    {
      "parameters": {
        "content": "### Triggers\nRun every 5 Minutes (schedule) or via manual test. Both paths go into the same flow.",
        "height": 672,
        "width": 256,
        "color": 7
      },
      "id": "090b7bfe-0ae5-42b9-bb99-adbbd3c8e90a",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        512
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 12
            }
          ]
        }
      },
      "id": "333918dd-a6b9-47cb-b9d5-1932557d8607",
      "name": "Run every 5 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        96,
        800
      ],
      "typeVersion": 1.2
    },
    {
      "parameters": {
        "content": "## Instance Selection\n\nDefines which environment this workflow reports metrics for.\n\nSet this value to match the current n8n instance\n(e.g. prod, dev, test).\n\nMetrics and snapshots will be tagged with this identifier\nso dashboards can separate environments correctly.\n",
        "height": 672,
        "width": 256,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        288,
        512
      ],
      "typeVersion": 1,
      "id": "edb81aaf-eb6d-4fe0-bf95-baecf014c886",
      "name": "Sticky Note9"
    }
  ],
  "connections": {
    "Trace Sync Trigger": {
      "main": [
        [
          {
            "node": "Set Instance Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Trace Checkpoint (last_id + refresh)": {
      "main": [
        [
          {
            "node": "Normalize Checkpoint Params",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Checkpoint Params": {
      "main": [
        [
          {
            "node": "Fetch Execution Headers from n8n",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Execution Headers from n8n": {
      "main": [
        [
          {
            "node": "Filter Specific Workflow (debug)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Specific Workflow (debug)": {
      "main": [
        [
          {
            "node": "Is Execution Data Small?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Execution Data Small?": {
      "main": [
        [
          {
            "node": "Collect Execution IDs (small only)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Header-Only Execution Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Execution IDs (small only)": {
      "main": [
        [
          {
            "node": "Chunk Execution IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chunk Execution IDs": {
      "main": [
        [
          {
            "node": "Load Execution Data + Workflow Nodes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Execution Data + Workflow Nodes": {
      "main": [
        [
          {
            "node": "Parse runData \u2192 Execution + Node Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse runData \u2192 Execution + Node Rows": {
      "main": [
        [
          {
            "node": "Route RowType (execution vs node)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route RowType (execution vs node)": {
      "main": [
        [
          {
            "node": "Upsert Execution Summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Upsert Execution Nodes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Header-Only Execution Row": {
      "main": [
        [
          {
            "node": "Generic Row Upsert (execution/node)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Workflows Metadata": {
      "main": [
        [
          {
            "node": "Normalize Workflow Index Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Workflow Index Rows": {
      "main": [
        [
          {
            "node": "Upsert Workflow Index",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Instance Context": {
      "main": [
        [
          {
            "node": "Load Workflows Metadata",
            "type": "main",
            "index": 0
          },
          {
            "node": "Load Trace Checkpoint (last_id + refresh)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run every 5 Minutes": {
      "main": [
        [
          {
            "node": "Set Instance Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}