Vector Store Node

The canvas node that reads and writes a Vector Store. One node does one of three things — insert content, query for it, or delete it — against a store you created in Settings. Everything it needs comes from the incoming payload through templates, so the same node indexes a scraped page, a Drive file, or a webhook body without changing shape.

The store itself — embedding model, credential, backend, default chunking — is configured once under Settings → Vector Stores. Start with the Vector Stores Quick Start if you have not created one yet.

First: pick a store

Nothing else on the node renders until a store is selected, and that is deliberate. The store is this node's credential: without it there is no embedding model, no dimensions, and no backend, so a Top K or a metadata filter configured beforehand would be a promise about something that does not exist yet. The picker can also create a store inline, with the same form as the settings page.


The three modes

ModeDoesOutput shapeIterable downstream
insertChunk, embed, and store incoming text and files{ chunksInserted, skipped, bytes, … }No
querySemantic search, returns the top matches{ matches: [...], tookMs }Yes — over matches
deleteRemove chunks by document id or metadata{ deletedCount }No

Every mode renders its fields as templates against the incoming payload, so {{payload.text}} works everywhere, as does a reference to another node in the same workspace.


Insert mode

Fields

  • Text to ingest — the rendered string is chunked with the store's chunking config, embedded, and inserted. Its content type is sniffed (JSON / CSV / Markdown / code / prose), so a template that renders a JSON array gets structure-aware chunking for free.
  • Source doc name — the human label that shows up in the Content tab and on every match.
  • Source doc id — the logical identity of the document (URL, hash, page id). This is what a later delete or upsert keys on.
  • Metadata — a JSON object stored on every chunk of this ingest, and the thing a query filter matches against.
jsonA typical insert node
Text to ingest      {{payload.article.body}}
Source doc name     {{payload.article.title}}
Source doc id       {{payload.article.url}}
Metadata            { "tenant": "{{payload.tenant}}", "lang": "en" }

Replace existing chunks for this doc

The toggle appears only once Source doc id has a value, because that is the only thing it can key on. When on, an ingest whose rendered sourceDocId already exists in the store deletes that document's prior chunks before inserting the new ones — an upsert by document.

  • Use it when the source changes. A Drive Action lists files hourly; when a file is edited, the new content lands and the stale chunks leave together, so the store never holds two versions of the same page.
  • Off (the default): chunks accumulate. The content-hash dedup still skips byte-identical chunks, but it never removes the outdated ones from a document that changed.
  • No-op when the id renders empty — the toggle only engages for ingests carrying a real id.

Files in the payload

With auto-extract on (the default), the node walks the incoming payload for _file references and pulls text out of PDFs, CSVs, JSON, and text/*. Images and other binaries are skipped with a log line and reported back in the output. Each extracted file becomes its own source, chunked by its own type, and every chunk keeps a sourceFile block so a match can be traced to the file it came from. Turn auto-extract off to use only the text template.

Advanced tab — per-type chunking overrides

Insert mode only. Leave every field empty and the store's chunking applies; fill one in and it wins for that content type. This is the only place in the product where the JSON limit can be changed.

TypeKnobs
PDFSize, Overlap
CSVRows per chunk, header repeated on each chunk
JSONMax chars per chunk (defaults to 4000 — the store's Size is not used on this path)
MarkdownSize, Overlap
CodeSize, Overlap
ProseSize, Overlap — the fallback for everything unrecognised

Max chars per file (-1 for unlimited) truncates each extracted file before chunking. It is the cheap way to cap what a 400-page PDF costs you in embeddings.

Insert output

jsonOutput payload — insert
{
  "_meta": { "iterable": false, "count": 1, "success": true, "mode": "insert" },
  "chunksInserted": 42,
  "skipped": 3,
  "bytes": 21014,
  "sourceCount": 2,
  "upsertedOver": 18,
  "warnings": ["json: value at chapters[4] exceeded the per-chunk limit and was split as text"]
}
  • skipped — chunks the dedup recognised as already present.
  • upsertedOver — only present when the upsert toggle actually deleted prior chunks.
  • warnings — only present when the chunker had to force something. Worth routing to a Filter node if this store feeds an agent.
  • skippedFiles — the files that produced no text, with the reason.
A good chunking and a broken one used to return identical counts. That is what warnings exists for — an automated flow can notice a bad ingest without anyone reading the logs.

When nothing extractable arrives, the node does not fail — it returns chunksInserted: 0 with a skippedReason such as empty textTemplate and no _file refs in payload.


Query mode

  • Query — the text to search for, usually {{payload.query}}.
  • Top K — maximum matches returned, capped at 50 server-side.
  • Min score — drop matches below this cosine similarity. Empty means whatever Top K gives you, however weak. For anything feeding an LLM, set it: 0.6 – 0.7 is a sane starting floor.
  • Metadata filter — a JSON object of scalar values matched against each chunk's metadata.
jsonOutput payload — query
{
  "_meta": { "iterable": true, "iterateField": "matches", "count": 3, "success": true, "mode": "query" },
  "matches": [
    {
      "chunkId": "665f...",
      "content": "Refunds are issued to the original payment method within 14 days…",
      "score": 0.871,
      "sourceDocId": "https://example.com/policies/returns",
      "sourceDocName": "Returns policy",
      "metadata": { "tenant": "acme", "lang": "en" }
    }
  ],
  "tookMs": 212
}

Because the output is marked iterable over matches, a Loop or Aggregator node downstream fans out over the passages with no glue in between. To hand the whole set to an AI Node instead, reference {{payload.matches}} in its prompt.


Delete mode

Takes a source doc id, a metadata filter, or both. At least one has to render to a non-empty value — otherwise the node deletes nothing and returns { deletedCount: 0, skippedReason: "no filter after template render" }. That guard is the difference between removing one document and emptying the store.


Metadata filters are data, not query structure

A rendered filter must be a flat object of scalar values. Objects, arrays, and any key beginning with $ are rejected, and the operation fails rather than running with a wider filter.

This is not pedantry about types. Templates substitute the actual value from the payload, so whatever arrives over a public webhook decides the shape of that value:
textWhy a filter must be scalars
template   { "customerId": "{{payload.customerId}}" }
payload    { "customerId": { "$ne": "__none__" } }
filter     { "customerId": { "$ne": "__none__" } }   ← matches everyone
In query mode that turns “this customer's chunks” into everybody's, and the matches flow onward to whatever is downstream. In delete mode it empties the store. Hence: scalars only, and a filter that fails to render aborts instead of degrading.

What each backend can filter on

  • PostgreSQL (pgvector) — any metadata key, no setup. Values are compared as text.
  • MongoDB Atlas — every filterable path has to be declared in the vector index. On the managed backend only the tenancy field is declared, so filtering by your own metadata key errors instead of returning nothing. On your own cluster, add the paths to your index definition and it works.

The Content tab

The node embeds the same chunk browser as the store detail page, so you can confirm what an insert actually produced without leaving the canvas — filter by source document name, read a full chunk, delete a bad one. It is the fastest way to answer “did my template render what I think it did”.


Patterns worth copying

Keep an index in sync with a source of truth

textScheduled → Drive Action → Vector Store (insert)
Scheduled Workflow (hourly)
  → Drive Action: list files in folder
  → Vector Store (insert)
       Text to ingest    (empty — files come through _file refs)
       Source doc id     {{payload.fileId}}
       Source doc name   {{payload.name}}
       Replace existing chunks for this doc: ON

Edited files replace themselves, untouched files cost nothing (the content hash skips them), and the store never accumulates two versions of a document.

Answer a question with retrieved context

textWebhook → Vector Store (query) → AI Node
Webhook
  → Vector Store (query)
       Query        {{payload.question}}
       Top K        5
       Min score    0.65
       Filter       { "tenant": "{{payload.tenant}}" }
  → AI Node
       Prompt       Answer using only this context: {{payload.matches}}

If the agent should decide when to search rather than always searching, skip the query node and attach the store to the AI Node as a tool instead — see As a tool on an AI Node.

Forget a document when the source deletes it

textWebhook (document.deleted) → Vector Store (delete)
Vector Store (delete)
  Source doc id   {{payload.documentId}}

Gotchas

  • Every query costs an embedding call on your own API key — the question has to be vectorised before it can be compared. A query node inside a loop over 500 items is 500 billed calls.
  • A paused store rejects both reads and writes. If a node started failing for no visible reason, check whether someone paused the store in Settings.
  • Chunk size is in characters, and the store's value — not the node's — governs the text template path. See Chunking.
  • An iterable input fans the node out. Feed it the output of another node marked iterable and it runs once per item, which is usually what you want for insert and rarely what you want for query.
  • The Advanced tab only applies in insert mode. In query and delete it shows a placeholder rather than settings that would do nothing.