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.
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
| Mode | Does | Output shape | Iterable downstream |
|---|---|---|---|
| insert | Chunk, embed, and store incoming text and files | { chunksInserted, skipped, bytes, … } | No |
| query | Semantic search, returns the top matches | { matches: [...], tookMs } | Yes — over matches |
| delete | Remove 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.
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.
| Type | Knobs |
|---|---|
| Size, Overlap | |
| CSV | Rows per chunk, header repeated on each chunk |
| JSON | Max chars per chunk (defaults to 4000 — the store's Size is not used on this path) |
| Markdown | Size, Overlap |
| Code | Size, Overlap |
| Prose | Size, 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
{
"_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.
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.
{
"_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.
template { "customerId": "{{payload.customerId}}" }
payload { "customerId": { "$ne": "__none__" } }
filter { "customerId": { "$ne": "__none__" } } ← matches everyoneWhat 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
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: ONEdited 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
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
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.