AutomationFlowsAI & RAG › Agente AI RAG

Agente AI RAG

Agente AI RAG. Uses lmChatOpenAi, documentDefaultDataLoader, embeddingsOpenAi, googleDrive. Event-driven trigger; 42 nodes.

Event trigger★★★★★ complexityAI-powered42 nodesOpenAI ChatDocument Default Data LoaderOpenAI EmbeddingsGoogle DriveGoogle Drive TriggerMemory Postgres ChatChat TriggerAgent
AI & RAG Trigger: Event Nodes: 42 Complexity: ★★★★★ AI nodes: yes Added:

This workflow follows the Agent → Chat Trigger recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "Agente AI RAG",
  "nodes": [
    {
      "parameters": {
        "model": "gpt-4.1-mini",
        "options": {}
      },
      "id": "97321b6b-47d8-4916-bba9-a061565a02b1",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1,
      "position": [
        -832,
        256
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsonMode": "expressionData",
        "jsonData": "={{ $json.content || $json.data || $json.text || $json.concatenated_data }}",
        "options": {
          "metadata": {
            "metadataValues": [
              {
                "name": "=file_id",
                "value": "={{ $('Set File ID').first().json.file_id }}"
              },
              {
                "name": "file_title",
                "value": "={{ $('Set File ID').first().json.file_title }}"
              }
            ]
          }
        }
      },
      "id": "ec793816-46f4-4c73-8d24-68eb95a9449e",
      "name": "Default Data Loader",
      "type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
      "typeVersion": 1,
      "position": [
        944,
        992
      ]
    },
    {
      "parameters": {
        "model": "text-embedding-3-small",
        "options": {}
      },
      "id": "919dd8d8-5fef-4102-b1ef-557e23aa2aa5",
      "name": "Embeddings OpenAI1",
      "type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
      "typeVersion": 1,
      "position": [
        736,
        992
      ],
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "content": "## re-ranking",
        "height": 369,
        "width": 295,
        "color": 6
      },
      "id": "24f2e2eb-f522-474b-ba29-e7a7239e6f8d",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -16
      ]
    },
    {
      "parameters": {
        "content": "## Aggiunta dei File al Vector DB",
        "height": 867,
        "width": 3073,
        "color": 5
      },
      "id": "39cde1f5-41f9-4d75-a387-ca782d25ed63",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1808,
        432
      ]
    },
    {
      "parameters": {
        "operation": "download",
        "fileId": {
          "__rl": true,
          "value": "={{ $('Set File ID').item.json.file_id }}",
          "mode": "id"
        },
        "options": {
          "googleFileConversion": {
            "conversion": {
              "docsToFormat": "text/plain"
            }
          }
        }
      },
      "id": "96731881-f189-49a4-9a62-ef6644ddf3b8",
      "name": "Download File",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        -720,
        720
      ],
      "executeOnce": true,
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "value": "1B_QtCPPIxr0zUcGd2kYeFKou4zgAK61T",
          "mode": "list",
          "cachedResultName": "RAG Material",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1B_QtCPPIxr0zUcGd2kYeFKou4zgAK61T"
        },
        "event": "fileCreated",
        "options": {}
      },
      "id": "c5cba60f-7ade-4a6a-9630-e9630ad0f58f",
      "name": "File Created",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "typeVersion": 1,
      "position": [
        -1744,
        560
      ],
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "value": "1B_QtCPPIxr0zUcGd2kYeFKou4zgAK61T",
          "mode": "list",
          "cachedResultName": "RAG Material",
          "cachedResultUrl": "https://drive.google.com/drive/folders/1B_QtCPPIxr0zUcGd2kYeFKou4zgAK61T"
        },
        "event": "fileUpdated",
        "options": {}
      },
      "id": "c4dcb3b8-b735-4abc-9dd0-371d10475e26",
      "name": "File Updated",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "typeVersion": 1,
      "position": [
        -1744,
        720
      ],
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "text",
        "options": {}
      },
      "id": "a5220f8a-af32-4043-8f78-5556f1996b8c",
      "name": "Extract Document Text",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        -16,
        1136
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {},
      "id": "6ee35cac-c97b-41fb-8a74-144ad1e32fd5",
      "name": "Postgres Chat Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryPostgresChat",
      "typeVersion": 1,
      "position": [
        -672,
        256
      ],
      "notesInFlow": false,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "10646eae-ae46-4327-a4dc-9987c2d76173",
              "name": "file_id",
              "value": "={{ $json.id }}",
              "type": "string"
            },
            {
              "id": "f4536df5-d0b1-4392-bf17-b8137fb31a44",
              "name": "file_type",
              "value": "={{ $json.mimeType }}",
              "type": "string"
            },
            {
              "id": "77d782de-169d-4a46-8a8e-a3831c04d90f",
              "name": "file_title",
              "value": "={{ $json.name }}",
              "type": "string"
            },
            {
              "id": "9bde4d7f-e4f3-4ebd-9338-dce1350f9eab",
              "name": "file_url",
              "value": "={{ $json.webViewLink }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "f90de6e5-27dd-4321-9639-600725706699",
      "name": "Set File ID",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1392,
        720
      ]
    },
    {
      "parameters": {
        "content": "## RAG AI Agent with Chat Interface",
        "height": 464.8027193303974,
        "width": 1035.6381264595484,
        "color": 2
      },
      "id": "3a278f7e-c0b5-4760-8c62-be77aa4ee89d",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1104,
        -48
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "9a9a245e-f1a1-4282-bb02-a81ffe629f0f",
              "name": "chatInput",
              "value": "={{ $json?.chatInput || $json.body.chatInput }}",
              "type": "string"
            },
            {
              "id": "b80831d8-c653-4203-8706-adedfdb98f77",
              "name": "sessionId",
              "value": "={{ $json?.sessionId || $json.body.sessionId}}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "6d56a38b-5803-49d4-bf24-1f4099fa44fb",
      "name": "Edit Fields",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -800,
        32
      ]
    },
    {
      "parameters": {
        "public": true,
        "options": {}
      },
      "id": "fc6054ec-e0c9-49c3-94c6-f28895c10497",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "typeVersion": 1.1,
      "position": [
        -1072,
        160
      ]
    },
    {
      "parameters": {
        "operation": "pdf",
        "options": {}
      },
      "id": "230705c7-07aa-4a30-b45c-28d1f99c42d4",
      "name": "Extract PDF Text",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        -16,
        960
      ]
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "options": {}
      },
      "id": "9334f3b9-4f83-4248-ab34-9c65f72c15d6",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        192,
        528
      ]
    },
    {
      "parameters": {
        "fieldsToSummarize": {
          "values": [
            {
              "aggregation": "concatenate",
              "field": "data"
            }
          ]
        },
        "options": {}
      },
      "id": "39473c61-ce67-420b-bb56-96bd3949d839",
      "name": "Summarize",
      "type": "n8n-nodes-base.summarize",
      "typeVersion": 1,
      "position": [
        400,
        608
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.chatInput }}",
        "options": {
          "systemMessage": "=Sei un assistente personale che aiuta a rispondere a domande basandosi su un corpus di documenti. I documenti sono o testuali (Txt, docs, PDF estratti, ecc.) o dati tabulari (CSV o documenti Excel).\n\nHai a disposizione strumenti per eseguire RAG nella tabella 'documents', consultare i documenti disponibili nella tua knowledge base nella tabella 'document_metadata', estrarre tutto il testo da un determinato documento e interrogare i file tabulari con SQL nella tabella 'document_rows'.\n\nInizia sempre eseguendo RAG a meno che la domanda non richieda una query SQL per dati tabulari (recuperare una somma, trovare un massimo, qualcosa per cui una ricerca RAG sarebbe inaffidabile). Se il RAG non aiuta, allora consulta i documenti che hai a disposizione, trova alcuni documenti che pensi possano contenere la risposta e poi analizzali.\n\nComunica sempre all'utente se non hai trovato la risposta. Non inventare qualcosa solo per compiacerlo."
        }
      },
      "id": "1a0f2d6f-fb7b-4baf-b95c-cd3c55504133",
      "name": "RAG AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 1.6,
      "position": [
        -592,
        32
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "2ae7faa7-a936-4621-a680-60c512163034",
                    "leftValue": "={{ $('Set File ID').item.json.file_type }} ",
                    "rightValue": "spreadsheetml",
                    "operator": {
                      "type": "string",
                      "operation": "contains"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "xlsx"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "fc193b06-363b-4699-a97d-e5a850138b0e",
                    "leftValue": "={{ $('Set File ID').item.json.file_type }}",
                    "rightValue": "=text/csv",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "csv"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "leftValue": "={{ $('Set File ID').item.json.file_type }}",
                    "rightValue": "application/pdf",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "id": "5fe78f9b-2ff3-4adc-954e-53bfce35c142"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "application/pdf"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 1
                },
                "conditions": [
                  {
                    "id": "b69f5605-0179-4b02-9a32-e34bb085f82d",
                    "leftValue": "={{ $('Set File ID').item.json.file_type }}",
                    "rightValue": "text/plain",
                    "operator": {
                      "type": "string",
                      "operation": "equals",
                      "name": "filter.operator.equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "text/plain"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "id": "a56e4f4b-4336-4f8e-91e2-9c8350d8232a",
      "name": "Switch",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        -528,
        688
      ]
    },
    {
      "parameters": {
        "operation": "xlsx",
        "options": {}
      },
      "id": "a9f7bcb8-682e-4129-9de3-3f5e7c8dc786",
      "name": "Extract from Excel",
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        -16,
        528
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f422e2e0-381c-46ea-8f38-3f58c501d8b9",
              "name": "schema",
              "value": "={{ $('Extract from Excel').isExecuted ? $('Extract from Excel').first().json.keys().toJsonString() : $('Extract from CSV').first().json.keys().toJsonString() }}",
              "type": "string"
            },
            {
              "id": "bb07c71e-5b60-4795-864c-cc3845b6bc46",
              "name": "data",
              "value": "={{ $json.concatenated_data }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        816,
        544
      ],
      "id": "59638715-1da4-4fd5-a1e0-fff25155b427",
      "name": "Set Schema"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1,
      "position": [
        -16,
        704
      ],
      "id": "77804135-0ba0-48a6-9d99-3e31daeaf3f8",
      "name": "Extract from CSV"
    },
    {
      "parameters": {
        "content": "## IMPORTANTE!\nRunna *entrambi* questi nodi per fare il setup delle tabelle del database!",
        "height": 300,
        "width": 680,
        "color": 3
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1808,
        112
      ],
      "typeVersion": 1,
      "id": "ba4d768c-bee7-4628-980f-2d987bcd9759",
      "name": "Sticky Note3"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "CREATE TABLE document_metadata (\n    id TEXT PRIMARY KEY,\n    title TEXT,\n    url TEXT,\n    created_at TIMESTAMP DEFAULT NOW(),\n    schema TEXT\n);",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        -1696,
        208
      ],
      "id": "5e3304c4-113c-477e-b973-b4a174af5844",
      "name": "Create Document Metadata Table",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "CREATE TABLE document_rows (\n    id SERIAL PRIMARY KEY,\n    dataset_id TEXT REFERENCES document_metadata(id),\n    row_data JSONB  -- Store the actual row data\n);",
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        -1392,
        208
      ],
      "id": "968bd293-b446-4f68-83d7-b3824bab21b0",
      "name": "Create Document Rows Table (for Tabular Data)",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Use this tool to fetch all available documents, including the table schema if the file is a CSV or Excel file.",
        "operation": "select",
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "document_metadata",
          "mode": "list",
          "cachedResultName": "document_metadata"
        },
        "returnAll": true,
        "options": {}
      },
      "type": "n8n-nodes-base.postgresTool",
      "typeVersion": 2.5,
      "position": [
        -528,
        256
      ],
      "id": "08c17a74-5e94-4576-8519-d3ea8bb2401c",
      "name": "List Documents",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Given a file ID, fetches the text from the document.",
        "operation": "executeQuery",
        "query": "SELECT \n    string_agg(text, ' ') as document_text\nFROM documents_pg\n  WHERE metadata->>'file_id' = $1\nGROUP BY metadata->>'file_id';",
        "options": {
          "queryReplacement": "={{ $fromAI('file_id') }}"
        }
      },
      "type": "n8n-nodes-base.postgresTool",
      "typeVersion": 2.5,
      "position": [
        -384,
        256
      ],
      "id": "be42a1e0-9874-47bc-ae13-783364014687",
      "name": "Get File Contents",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "=Esegui query SQL sulla tabella document_rows per analizzare dati da file Excel/CSV caricati.\n\nSTRUTTURA TABELLA:\n- id: integer (chiave primaria)\n- dataset_id: text (ID univoco del file, formato hash tipo \"1d4V8jQ-GahMa...\")\n- row_data: jsonb (contiene tutti i dati della riga come oggetto JSON)\n\nCOME ACCEDERE AI DATI:\nI valori delle colonne sono dentro row_data come JSON. Usa:\n- row_data->'NomeColonna' per estrarre come JSON (mantiene tipo numero/boolean)\n- row_data->>'NomeColonna' per estrarre come testo\n\nESEMPI QUERY CORRETTE:\n\n1. Somma numerica (usa -> per numeri):\nSELECT SUM((row_data->'Fatturato')::numeric) as totale\nFROM document_rows \nWHERE dataset_id = 'INSERISCI_QUI_IL_DATASET_ID';\n\n2. Contare righe:\nSELECT COUNT(*) as totale_righe\nFROM document_rows \nWHERE dataset_id = 'INSERISCI_DATASET_ID';\n\n3. Media valori:\nSELECT AVG((row_data->'Fatturato')::numeric) as media\nFROM document_rows \nWHERE dataset_id = 'ID_DEL_FILE';\n\n4. Filtrare per valore testuale (usa ->> per testo):\nSELECT row_data\nFROM document_rows \nWHERE dataset_id = 'ID_DEL_FILE'\nAND row_data->>'Codice' = 'PROD-2001';\n\n5. Filtrare per valore numerico (usa -> per numeri):\nSELECT row_data\nFROM document_rows \nWHERE dataset_id = 'ID_DEL_FILE'\nAND (row_data->'Fatturato')::numeric > 1000;\n\n6. Aggregazioni multiple:\nSELECT \n  COUNT(*) as totale_prodotti,\n  SUM((row_data->'Fatturato')::numeric) as fatturato_totale,\n  AVG((row_data->'Unit\u00e0 Vendute')::numeric) as media_vendite\nFROM document_rows \nWHERE dataset_id = 'ID_DEL_FILE';\n\nREGOLA GROUP BY:\n- SUM/AVG/COUNT da soli = totale globale \u2192 NO GROUP BY necessario \u2705\n- SUM/AVG/COUNT + altre colonne = aggregazione per gruppo \u2192 SERVE GROUP BY \u2705\n\nNOTA DATASET_ID:\nIl dataset_id NON \u00e8 il nome del file ma un ID hash univoco (es: \"1d4V8jQ-GahMa-YmOQ7JJjY2-g...\").\nUsa il valore passato dal workflow, NON il nome del file.",
        "operation": "executeQuery",
        "query": "{{ $fromAI('sql_query') }}",
        "options": {}
      },
      "type": "n8n-nodes-base.postgresTool",
      "typeVersion": 2.5,
      "position": [
        -224,
        256
      ],
      "id": "daec4b4f-431e-4e68-a0c4-2c280892fdfa",
      "name": "Query Document Rows",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
      "typeVersion": 1.2,
      "position": [
        32,
        192
      ],
      "id": "dec0533f-4e3a-40aa-8d92-c3267f8281e9",
      "name": "Embeddings OpenAI2",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -1568,
        560
      ],
      "id": "75d7a7b9-dac1-41b1-975f-8eb3bf1189f1",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "operation": "upsert",
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "document_metadata",
          "mode": "list",
          "cachedResultName": "document_metadata"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": "={{ $('Set File ID').item.json.file_id }}",
            "title": "={{ $('Set File ID').item.json.file_title }}",
            "url": "={{ $('Set File ID').item.json.file_url }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": true,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "title",
              "displayName": "title",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false
            },
            {
              "id": "url",
              "displayName": "url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false,
              "removed": false
            },
            {
              "id": "created_at",
              "displayName": "created_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": false
            },
            {
              "id": "schema",
              "displayName": "schema",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false,
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        -880,
        576
      ],
      "id": "2c18f080-98d6-4d74-ad7c-c19fa373d854",
      "name": "Insert Document Metadata",
      "executeOnce": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "document_rows",
          "mode": "list",
          "cachedResultName": "document_rows"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "dataset_id": "={{ $('Set File ID').item.json.file_id }}",
            "row_data": "={{ $json.toJsonString().replaceAll(/'/g, \"''\") }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "removed": true
            },
            {
              "id": "dataset_id",
              "displayName": "dataset_id",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "row_data",
              "displayName": "row_data",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "object",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        192,
        704
      ],
      "id": "5705aff1-ccd2-4302-bb08-7a617525ddab",
      "name": "Insert Table Rows",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "upsert",
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "table": {
          "__rl": true,
          "value": "document_metadata",
          "mode": "list",
          "cachedResultName": "document_metadata"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": "={{ $('Set File ID').item.json.file_id }}",
            "schema": "={{ $json.schema }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": true,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "title",
              "displayName": "title",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false,
              "removed": true
            },
            {
              "id": "url",
              "displayName": "url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false,
              "removed": true
            },
            {
              "id": "created_at",
              "displayName": "created_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "dateTime",
              "canBeUsedToMatch": false
            },
            {
              "id": "schema",
              "displayName": "schema",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": false,
              "removed": false
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1040,
        544
      ],
      "id": "65ddb610-7e08-4e0e-8dff-f35e707318ef",
      "name": "Update Schema for Document Metadata",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "content": "# \ud83d\ude80 Template per RAG Agentico in n8n\n\n**Autore:** [Riccardo Belli Contarini](https://www.youtube.com/@riccardobellicontarini)\n\n## Cos'\u00e8 questo template?\n\nQuesto template fornisce un'implementazione completa di un sistema **Agentic RAG (Retrieval Augmented Generation)** in n8n che pu\u00f2 essere facilmente esteso per il tuo caso d'uso specifico e la tua knowledge base. A differenza del RAG standard che esegue solo semplici ricerche, questo agente pu\u00f2 ragionare sulla tua knowledge base, migliorare autonomamente il recupero delle informazioni e passare dinamicamente tra diversi strumenti in base alla domanda specifica. Questo agente utilizza Postgres con PGVector per la knowledge base. Puoi usare Supabase, Neon, Postgres self-hosted, ecc.\n\n## Perch\u00e9 Agentic RAG?\n\nIl RAG standard ha limitazioni significative:\n- Analisi scarsa di dati numerici/tabulari\n- Contesto mancante a causa della suddivisione dei documenti in chunk\n- Incapacit\u00e0 di collegare informazioni tra documenti diversi\n- Nessuna selezione dinamica degli strumenti in base al tipo di domanda\n\n## Cosa rende potente questo template:\n\n- **Selezione intelligente degli strumenti**: Passa automaticamente tra ricerche RAG, query SQL o recupero di documenti completi in base alla domanda\n- **Contesto documentale completo**: Accede a interi documenti quando necessario invece che solo a frammenti\n- **Analisi numerica accurata**: Utilizza SQL per calcoli precisi su dati in formato foglio di calcolo/tabulari\n- **Insight cross-documentali**: Collega informazioni attraverso l'intera knowledge base\n- **Elaborazione multi-file**: Gestisce pi\u00f9 documenti in un singolo ciclo di workflow\n- **Archiviazione efficiente**: Utilizza JSONB in Postgres per memorizzare dati tabulari senza creare nuove tabelle per ogni CSV\n\n## Come iniziare\n\n1. Esegui prima i nodi di creazione tabelle per configurare le tabelle del database in Postgres\n2. Carica i tuoi documenti tramite Google Drive (o sostituisci con una soluzione di archiviazione file diversa)\n3. L'agente li elaborer\u00e0 automaticamente (suddividendo il testo in chunk, memorizzando i dati tabulari in Postgres)\n4. Inizia a fare domande che sfruttano i molteplici approcci di ragionamento dell'agente\n\n## Personalizzazione\n\nQuesto template fornisce una base solida che puoi estendere:\n- Ottimizzando il prompt di sistema per il tuo caso d'uso specifico\n- Aggiungendo metadati ai documenti come riassunti\n- Implementando tecniche RAG pi\u00f9 avanzate\n- Ottimizzando per knowledge base pi\u00f9 grandi",
        "height": 1192,
        "width": 540,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2384,
        112
      ],
      "typeVersion": 1,
      "id": "43fccf0f-8dd5-4612-b0df-d7c8cf9e663a",
      "name": "Sticky Note9"
    },
    {
      "parameters": {
        "topN": 4
      },
      "type": "@n8n/n8n-nodes-langchain.rerankerCohere",
      "typeVersion": 1,
      "position": [
        160,
        192
      ],
      "id": "bd3217f2-4a8c-4694-a6c9-c1422f77de0b",
      "name": "Reranker Cohere",
      "credentials": {
        "cohereApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "retrieve-as-tool",
        "toolDescription": "Use RAG to look up information in the knowledgebase.",
        "tableName": "documents_pg",
        "topK": 25,
        "useReranker": true,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.vectorStorePGVector",
      "typeVersion": 1.3,
      "position": [
        32,
        80
      ],
      "id": "140c1952-8088-4611-979f-537692d8662d",
      "name": "Postgres PGVector Store",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "insert",
        "tableName": "documents_pg",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.vectorStorePGVector",
      "typeVersion": 1.3,
      "position": [
        816,
        736
      ],
      "id": "d2407b9b-270a-4f31-9de7-71ccaac9965f",
      "name": "Postgres PGVector Store1",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "DO $$\nBEGIN\n    IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'documents_pg') THEN\n        EXECUTE 'DELETE FROM documents_pg WHERE metadata->>''file_id'' LIKE ''%' || $1 || '%''';\n    END IF;\nEND\n$$;",
        "options": {
          "queryReplacement": "={{ $json.file_id }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -1216,
        576
      ],
      "id": "a85d4e51-ba1d-48b3-8de3-a57cf1aa3085",
      "name": "Delete Old Data Rows",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "DELETE FROM document_rows\nWHERE dataset_id LIKE '%' || $1 || '%';",
        "options": {
          "queryReplacement": "={{ $('Set File ID').item.json.file_id }}"
        }
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        -1040,
        736
      ],
      "id": "2dffd4dc-dc01-4a9e-99f1-92411dfdc654",
      "name": "Delete Old Doc Rows",
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
      "typeVersion": 1,
      "position": [
        1024,
        1136
      ],
      "id": "2c3cc29a-7c08-4c92-bfd6-484b2536e7d1",
      "name": "Recursive Character Text Splitter"
    },
    {
      "parameters": {
        "code": {
          "execute": {
            "code": "const { PromptTemplate } = require('@langchain/core/prompts');\n\nconst documentContent = $input.item.json?.data || $input.item.json?.text;\nconst maxChunkSize = 1000;\nconst minChunkSize = 400;\n\nif (!documentContent) {\n    throw new Error('No document found in input');\n}\n\nconst llm = await this.getInputConnectionData('ai_languageModel', 0);\n\nfunction cleanText(text) {\n    return text.replace(/\\s+/g, ' ').trim();\n}\n\nconst chunks = [];\nlet remainingText = cleanText(documentContent);\nlet chunkNumber = 1;\n\nif (remainingText.length <= maxChunkSize) {\n    chunks.push({\n        content: remainingText,\n        chunk: chunkNumber,\n        chunk_size: remainingText.length\n    });\n} else {\n    while (remainingText) {\n        const textToAnalyze = remainingText.substring(0, maxChunkSize);\n        \n        const promptText = `You are analyzing a document to find the best transition point to split it into meaningful sections.\n\nYour goal: Keep related content together and split where topics naturally transition.\n\nRead this text carefully and identify where one topic/section ends and another begins:\n\n${textToAnalyze}\n\nFind the best transition point that occurs BEFORE character position ${maxChunkSize}.\n\nLook for:\n- Section headings or topic changes\n- Paragraph boundaries where the subject shifts\n- Complete conclusions before new ideas start\n- Natural breaks between different aspects of the content\n\nOutput the LAST WORD that appears right before your chosen split point.\nJust the single word itself, nothing else.\nExample: If you want to split after \"The company was founded in 2022.\" then output: \"2022\"`;\n        \n        const prompt = PromptTemplate.fromTemplate(promptText);\n        const chain = prompt.pipe(llm);\n        \n        let breakPoint = maxChunkSize;\n        \n        try {\n            const response = await chain.invoke();\n            const responseText = response.content || response.text || response.toString();\n            const breakWord = responseText.trim();\n            \n            if (breakWord) {\n                // Find the last occurrence of this word in the text to analyze\n                const wordIndex = textToAnalyze.lastIndexOf(breakWord);\n                if (wordIndex !== -1) {\n                    // Split after the word (including any punctuation that follows)\n                    breakPoint = wordIndex + breakWord.length;\n                    // Move past any punctuation or single space after the word\n                    while (breakPoint < textToAnalyze.length && \n                           (textToAnalyze[breakPoint] === '.' || \n                            textToAnalyze[breakPoint] === '!' || \n                            textToAnalyze[breakPoint] === '?' || \n                            textToAnalyze[breakPoint] === ',' || \n                            textToAnalyze[breakPoint] === ';' || \n                            textToAnalyze[breakPoint] === ':' || \n                            textToAnalyze[breakPoint] === ' ')) {\n                        breakPoint++;\n                        // Stop after moving past one space\n                        if (textToAnalyze[breakPoint - 1] === ' ') break;\n                    }\n                    breakPoint = Math.min(breakPoint, maxChunkSize);\n                }\n            }\n        } catch (error) {\n            console.log('LLM failed to determine breakpoint, using max size:', error.message);\n            breakPoint = maxChunkSize;\n        }\n        \n        const chunk = remainingText.substring(0, breakPoint).trim();\n        \n        if (chunk) {\n            chunks.push({\n                content: chunk,\n                chunk: chunkNumber,\n                chunk_size: chunk.length\n            });\n            chunkNumber++;\n        }\n        \n        remainingText = remainingText.substring(breakPoint).trim();\n        \n        if (!remainingText) {\n            break;\n        }\n    }\n}\n\n// Merge chunks that are below minimum size with adjacent chunks if possible\nlet i = 0;\nwhile (i < chunks.length) {\n    if (chunks[i].chunk_size < minChunkSize) {\n        // Try to merge with next chunk first if it exists and won't exceed max\n        if (i + 1 < chunks.length && \n            chunks[i].chunk_size + chunks[i + 1].chunk_size <= maxChunkSize) {\n            // Merge current with next\n            chunks[i].content += ' ' + chunks[i + 1].content;\n            chunks[i].chunk_size = chunks[i].content.length;\n            chunks.splice(i + 1, 1);\n            // Don't increment i, check this chunk again in case it's still small\n        } \n        // Otherwise try to merge with previous chunk if it exists and won't exceed max\n        else if (i > 0 && \n                 chunks[i - 1].chunk_size + chunks[i].chunk_size <= maxChunkSize) {\n            // Merge current into previous\n            chunks[i - 1].content += ' ' + chunks[i].content;\n            chunks[i - 1].chunk_size = chunks[i - 1].content.length;\n            chunks.splice(i, 1);\n            // Don't increment i, we removed current chunk\n        } else {\n            // Can't merge without exceeding max, move on\n            i++;\n        }\n    } else {\n        i++;\n    }\n}\n\nconst returnData = chunks.map(chunk => ({\n    json: chunk\n}));\n\nreturn returnData;"
          }
        },
        "inputs": {
          "input": [
            {
              "type": "ai_languageModel",
              "maxConnections": 1,
              "required": true
            },
            {
              "type": "main",
              "required": true
            }
          ]
        },
        "outputs": {
          "output": [
            {
              "type": "main"
            }
          ]
        }
      },
      "type": "@n8n/n8n-nodes-langchain.code",
      "typeVersion": 1,
      "position": [
        256,
        960
      ],
      "id": "f882db94-7478-4ae6-b9ac-18442eddd2de",
      "name": "LangChain Code"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "value": "gpt-4.1",
          "mode": "list",
          "cachedResultName": "gpt-4.1"
        },
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        336,
        1152
      ],
      "id": "11861d1b-7873-40c3-a827-75578632af55",
      "name": "OpenAI Chat Model1",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "RAG AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Download File": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "File Created": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Document Text": {
      "main": [
        [
          {
            "node": "LangChain Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Embeddings OpenAI1": {
      "ai_embedding": [
        [
          {
            "node": "Postgres PGVector Store1",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Default Data Loader": {
      "ai_document": [
        [
          {
            "node": "Postgres PGVector Store1",
            "type": "ai_document",
            "index": 0
          }
        ]
      ]
    },
    "Postgres Chat Memory": {
      "ai_memory": [
        [
          {
            "node": "RAG AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Set File ID": {
      "main": [
        [
          {
            "node": "Delete Old Data Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "File Updated": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "RAG AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract PDF Text": {
      "main": [
        [
          {
            "node": "LangChain Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Summarize",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize": {
      "main": [
        [
          {
            "node": "Set Schema",
            "type": "main",
            "index": 0
          },
          {
            "node": "Postgres PGVector Store1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RAG AI Agent": {
      "main": [
        []
      ]
    },
    "Switch": {
      "main": [
        [
          {
            "node": "Extract from Excel",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract from CSV",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract PDF Text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract Document Text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Extract Document Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from Excel": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          },
          {
            "node": "Insert Table Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Schema": {
      "main": [
        [
          {
            "node": "Update Schema for Document Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract from CSV": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          },
          {
            "node": "Insert Table Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Documents": {
      "ai_tool": [
        [
          {
            "node": "RAG AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Get File Contents": {
      "ai_tool": [
        [
          {
            "node": "RAG AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Query Document Rows": {
      "ai_tool": [
        [
          {
            "node": "RAG AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Embeddings OpenAI2": {
      "ai_embedding": [
        [
          {
            "node": "Postgres PGVector Store",
            "type": "ai_embedding",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Set File ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert Document Metadata": {
      "main": [
        [
          {
            "node": "Download File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reranker Cohere": {
      "ai_reranker": [
        [
          {
            "node": "Postgres PGVector Store",
            "type": "ai_reranker",
            "index": 0
          }
        ]
      ]
    },
    "Postgres PGVector Store": {
      "ai_tool": [
        [
          {
            "node": "RAG AI Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Postgres PGVector Store1": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete Old Data Rows": {
      "main": [
        [
          {
            "node": "Delete Old Doc Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete Old Doc Rows": {
      "main": [
        [
          {
            "node": "Insert Document Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Recursive Character Text Splitter": {
      "ai_textSplitter": [
        [
          {
            "node": "Default Data Loader",
            "type": "ai_textSplitter",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "LangChain Code",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "LangChain Code": {
      "main": [
        [
          {
            "node": "Postgres PGVector Store1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": false
  },
  "versionId": "fad8295c-c25a-4b75-baa7-09c54ce7327d",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "DAxUXUF9lAAImvGLVzMIk",
  "tags": []
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Agente AI RAG. Uses lmChatOpenAi, documentDefaultDataLoader, embeddingsOpenAi, googleDrive. Event-driven trigger; 42 nodes.

Source: https://github.com/tommyblink182/n8n/blob/93826053b0815bdc248d4b85b0a97b48ada9a964/workflows/Agente_AI_RAG.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

Your AI workforce is ready. Are you?

Google Sheets Tool, Mcp Trigger, Google Drive +29
AI & RAG

Agent IA Projet Client. Uses executeWorkflowTrigger, lmChatOpenAi, toolWorkflow, vectorStoreQdrant. Event-driven trigger; 79 nodes.

Execute Workflow Trigger, OpenAI Chat, Tool Workflow +16
AI & RAG

This intelligent chatbot leverages cutting-edge financial APIs and AI-driven analysis to deliver comprehensive stock research reports. Get instant access to professional-grade investment analysis that

Tool Think, Supabase Vector Store, OpenAI Embeddings +15
AI & RAG

This n8n template automatically classifies incoming emails (Sales, Support, Internal, Finance, Promotions) and routes them to a dedicated OpenAI LLM Agent for processing. Depending on the category, th

OpenAI, Gmail, Text Classifier +16
AI & RAG

Automate Outreach Prospect automates finding, enriching, and messaging potential partners (like restaurants, malls, and bars) using Apify Google Maps scraping, Perplexity enrichment, OpenAI LLMs, Goog

@Devlikeapro/N8N Nodes Waha, Google Drive Trigger, @Apify/N8N Nodes Apify +14