{
  "name": "01 \u2014 Ingest NIST CSF 2.0",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ingest-nist-csf",
        "responseMode": "lastNode",
        "options": {}
      },
      "id": "a1000000-0000-4000-8000-000000000001",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -640,
        0
      ],
      "notes": "A webhook rather than a manual trigger so the run is reproducible from a script."
    },
    {
      "parameters": {
        "fileSelector": "/data/docs/NIST.CSWP.29.pdf",
        "options": {}
      },
      "id": "a1000000-0000-4000-8000-000000000002",
      "name": "Read PDF",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [
        -420,
        0
      ],
      "notes": "Mounted read-only. Only this PDF \u2014 the other two stay unprocessed to control cost."
    },
    {
      "parameters": {
        "operation": "pdf",
        "options": {}
      },
      "id": "a1000000-0000-4000-8000-000000000003",
      "name": "Extract PDF Text",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        -200,
        0
      ]
    },
    {
      "parameters": {
        "mode": "insert",
        "tableName": "documents",
        "options": {
          "queryName": "match_documents"
        }
      },
      "id": "a1000000-0000-4000-8000-000000000004",
      "name": "Supabase Vector Store",
      "type": "@n8n/n8n-nodes-langchain.vectorStoreSupabase",
      "typeVersion": 1.1,
      "position": [
        20,
        0
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "notes": "Table and query name must match sql/001_documents_and_match.sql."
    },
    {
      "parameters": {
        "model": "text-embedding-3-small",
        "options": {}
      },
      "id": "a1000000-0000-4000-8000-000000000005",
      "name": "Embeddings OpenAI",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
      "typeVersion": 1.2,
      "position": [
        -60,
        220
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "notes": "1536 dimensions \u2014 pinned by the documents.embedding column width."
    },
    {
      "parameters": {
        "dataType": "json",
        "jsonMode": "expressionData",
        "jsonData": "={{ $json.text }}",
        "textSplittingMode": "custom",
        "options": {
          "metadata": {
            "metadataValues": [
              {
                "name": "source",
                "value": "NIST.CSWP.29.pdf"
              },
              {
                "name": "title",
                "value": "NIST Cybersecurity Framework 2.0"
              }
            ]
          }
        }
      },
      "id": "a1000000-0000-4000-8000-000000000006",
      "name": "Default Data Loader",
      "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
      "typeVersion": 1,
      "position": [
        120,
        220
      ],
      "notes": "dataType only accepts json or binary \u2014 there is no text mode. Feeding the extracted text through jsonData is what routes the PDF body to the custom splitter; the default (allInputData) hands the loader the whole item and it splits per line, producing ~99-character fragments."
    },
    {
      "parameters": {
        "chunkSize": 1000,
        "chunkOverlap": 150,
        "options": {}
      },
      "id": "a1000000-0000-4000-8000-000000000007",
      "name": "Recursive Character Text Splitter",
      "type": "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
      "typeVersion": 1,
      "position": [
        120,
        420
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "tableId": "documents",
        "returnAll": true,
        "orderBy": "id.asc",
        "filters": {}
      },
      "id": "a1000000-0000-4000-8000-000000000008",
      "name": "Read Back Rows",
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        280,
        0
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "notes": "executeOnce matters: without it this runs once per inserted chunk, and 187 full-table reads with 1536 floats per row is what tripped the task runner.",
      "executeOnce": true
    },
    {
      "parameters": {
        "jsCode": "// Row count is exact here: executeOnce plus returnAll means one full read.\n// max(id) would be wrong after a reset \u2014 DELETE does not rewind the bigserial.\nconst rows = $input.all().map((r) => r.json);\nconst lengths = rows.map((r) => (r.content ?? '').length);\nconst first = rows[0] ?? {};\nconst embedding = typeof first.embedding === 'string' ? JSON.parse(first.embedding) : first.embedding;\nreturn [{ json: {\n  rowCount: rows.length,\n  embeddingDimensions: Array.isArray(embedding) ? embedding.length : null,\n  chunkChars: { min: Math.min(...lengths), max: Math.max(...lengths), mean: Math.round(lengths.reduce((a,b)=>a+b,0)/lengths.length), total: lengths.reduce((a,b)=>a+b,0) },\n  sampleChunk: (first.content ?? '').slice(0, 600),\n  sampleMetadata: first.metadata ?? null,\n} }];"
      },
      "id": "a1000000-0000-4000-8000-000000000009",
      "name": "Summarise",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        0
      ],
      "notes": "No executeOnce here: it would clamp the node to the first item and report a row count of 1."
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Read PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read PDF": {
      "main": [
        [
          {
            "node": "Extract PDF Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract PDF Text": {
      "main": [
        [
          {
            "node": "Supabase Vector Store",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supabase Vector Store": {
      "main": [
        [
          {
            "node": "Read Back Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Back Rows": {
      "main": [
        [
          {
            "node": "Summarise",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Embeddings OpenAI": {
      "ai_embedding": [
        [
          {
            "node": "Supabase Vector Store",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Default Data Loader": {
      "ai_document": [
        [
          {
            "node": "Supabase Vector Store",
            "type": "ai_document",
            "index": 0
          }
        ]
      ]
    },
    "Recursive Character Text Splitter": {
      "ai_textSplitter": [
        [
          {
            "node": "Default Data Loader",
            "type": "ai_textSplitter",
            "index": 0
          }
        ]
      ]
    }
  }
}