{
  "name": "Workflow B — Synthesize & Store Clusters",
  "nodes": [
    {
      "parameters": {},
      "id": "node_wf_trigger",
      "name": "Workflow Trigger",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1,
      "position": [
        200,
        300
      ],
      "notes": "Activated by Workflow A's Trigger node. Also run manually to test."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT id, title, source_name, source_type, url, raw_content, topic_tag, published_date FROM raw_intelligence WHERE processed = FALSE ORDER BY topic_tag, pulled_date DESC;",
        "options": {}
      },
      "id": "node_fetch_unprocessed",
      "name": "Fetch Unprocessed Items",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.3,
      "position": [
        440,
        300
      ],
      "notes": "ADD YOUR DETAILS: Select your Postgres credential."
    },
    {
      "parameters": {
        "language": "python",
        "jsCode": "PASTE_GROUP_CODE_HERE"
      },
      "id": "node_group",
      "name": "Group By Topic Tag",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ],
      "notes": "Groups unprocessed items by topic_tag. Each group becomes one Claude call."
    },
    {
      "parameters": {
        "model": "claude-opus-4-5",
        "options": {
          "temperature": 0.3,
          "maxTokensToSample": 1024
        },
        "messages": {
          "values": [
            {
              "content": "You are an AI industry intelligence analyst. You receive a batch of news articles, community posts, and research papers all related to the same topic category.\n\nSynthesize them into a single intelligence cluster.\n\nRespond ONLY in valid JSON with no extra text, no markdown, no code fences:\n{\n  \"cluster_title\": \"Concise descriptive headline max 12 words\",\n  \"summary\": \"4-6 sentence synthesis covering key developments, why they matter, and notable reactions or implications\",\n  \"confidence_score\": 8,\n  \"top_source_urls\": [\"url1\", \"url2\", \"url3\"]\n}\n\nConfidence score: 9-10 = multiple high quality corroborating sources, 7-8 = solid sources with some gaps, 5-6 = limited or mixed sources, 1-4 = sparse or low quality signals."
            },
            {
              "role": "user",
              "content": "=Topic Category: {{ $json.topic_tag }}\nTotal Items: {{ $json.item_count }}\n\nItems:\n\n{{ $json.combined_text }}"
            }
          ]
        }
      },
      "id": "node_claude",
      "name": "Claude — Synthesize Cluster",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        920,
        300
      ],
      "notes": "ADD YOUR DETAILS: Add Anthropic API key in n8n credentials."
    },
    {
      "parameters": {
        "language": "python",
        "jsCode": "PASTE_PARSE_CODE_HERE"
      },
      "id": "node_parse_response",
      "name": "Parse Claude Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1160,
        300
      ],
      "notes": "Safely parses Claude JSON output. Handles errors so workflow never breaks."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO topic_clusters (cluster_title, topic_tag, summary, source_urls, item_count, confidence_score, created_date, phase2_status) VALUES ('{{ $json.cluster_title.replace(/'/g, \"''\") }}', '{{ $json.topic_tag }}', '{{ $json.summary.replace(/'/g, \"''\") }}', '{{ $json.source_urls }}', {{ $json.item_count }}, {{ $json.confidence_score }}, NOW(), 'pending') RETURNING id, cluster_title, topic_tag, confidence_score;",
        "options": {}
      },
      "id": "node_insert_cluster",
      "name": "Store — Insert Cluster",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.3,
      "position": [
        1400,
        300
      ],
      "notes": "ADD YOUR DETAILS: Select your Postgres credential. RETURNING gives back the new cluster ID."
    },
    {
      "parameters": {
        "language": "python",
        "jsCode": "PASTE_BUILD_LINKS_CODE_HERE"
      },
      "id": "node_build_links",
      "name": "Build Cluster Source Links",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1640,
        200
      ],
      "notes": "Prepares link records between cluster and raw items."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO cluster_sources (cluster_id, raw_intelligence_id) VALUES ({{ $json.cluster_id }}, {{ $json.raw_id }}) ON CONFLICT DO NOTHING;",
        "options": {}
      },
      "id": "node_insert_links",
      "name": "Store — Insert Source Links",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.3,
      "position": [
        1880,
        200
      ],
      "notes": "ADD YOUR DETAILS: Select your Postgres credential."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE raw_intelligence SET processed = TRUE WHERE id IN (SELECT unnest(string_to_array('{{ $json.ids_str }}', ','))::int);",
        "options": {}
      },
      "id": "node_mark_processed",
      "name": "Mark Raw Items Processed",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.3,
      "position": [
        1640,
        400
      ],
      "notes": "ADD YOUR DETAILS: Select your Postgres credential. Flips processed=TRUE on all raw items consumed by this cluster."
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT id, cluster_title, topic_tag, summary, confidence_score, item_count, source_urls, phase2_status, created_date FROM topic_clusters WHERE phase2_status = 'pending' AND created_date >= NOW() - INTERVAL '5 hours' ORDER BY confidence_score DESC;",
        "options": {}
      },
      "id": "node_fetch_digest",
      "name": "Fetch Clusters for Webhook",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.3,
      "position": [
        1880,
        400
      ],
      "notes": "ADD YOUR DETAILS: Select your Postgres credential. Pulls all pending clusters from last 5 hours."
    },
    {
      "parameters": {
        "language": "python",
        "jsCode": "PASTE_FORMAT_OUTPUT_CODE_HERE"
      },
      "id": "node_format_output",
      "name": "Format Webhook Output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2120,
        400
      ],
      "notes": "Formats clusters into clean JSON array for the dashboard."
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ai-intelligence-digest",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "node_webhook",
      "name": "Webhook — Receive Request",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        2360,
        300
      ],
      "notes": "Your webhook URL will be: https://YOUR_N8N_DOMAIN/webhook/ai-intelligence-digest"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              },
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "id": "node_webhook_response",
      "name": "Webhook — Send Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2600,
        300
      ],
      "notes": "Sends cluster JSON to the frontend dashboard."
    },
    {
      "id": "44ba813f-90af-5041-868b-bd2411994094",
      "name": "Publication · Overview and setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -800,
        0
      ],
      "parameters": {
        "content": "# Workflow B — Synthesize & Store Clusters\n\n## Purpose and structure\nThis folder contains the **Workflow B — Synthesize & Store Clusters** workflow. Its executable nodes use `code`, `executeWorkflowTrigger`, `lmChatAnthropic`, `postgres`, `respondToWebhook`, `webhook`. The table and connection map below are derived from the JSON, not an inferred guarantee of behavior.\n\n## Setup\nImport inactive. Configure your own credentials and replace REPLACE_WITH_* values. Review external destinations and Code nodes before running.\n\n## Entry points\nWorkflow Trigger, Webhook — Receive Request\n\n## Verification\nSanitized source candidate; not runtime-tested. This overview is generated from node metadata. Read the folder README for the connection map, credential types, and license.\n\n## Attribution\nNik Bear Brown; rights confirmed 2026-10-19\nMIT; preserve source notices.",
        "width": 700,
        "height": 800,
        "color": 1
      }
    }
  ],
  "connections": {
    "Workflow Trigger": {
      "main": [
        [
          {
            "node": "Fetch Unprocessed Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Unprocessed Items": {
      "main": [
        [
          {
            "node": "Group By Topic Tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group By Topic Tag": {
      "main": [
        [
          {
            "node": "Claude — Synthesize Cluster",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude — Synthesize Cluster": {
      "main": [
        [
          {
            "node": "Parse Claude Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Claude Response": {
      "main": [
        [
          {
            "node": "Store — Insert Cluster",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store — Insert Cluster": {
      "main": [
        [
          {
            "node": "Build Cluster Source Links",
            "type": "main",
            "index": 0
          },
          {
            "node": "Mark Raw Items Processed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Cluster Source Links": {
      "main": [
        [
          {
            "node": "Store — Insert Source Links",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Raw Items Processed": {
      "main": [
        [
          {
            "node": "Fetch Clusters for Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Clusters for Webhook": {
      "main": [
        [
          {
            "node": "Format Webhook Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Webhook Output": {
      "main": [
        [
          {
            "node": "Webhook — Receive Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook — Receive Request": {
      "main": [
        [
          {
            "node": "Webhook — Send Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true
  },
  "active": false
}
