Cardinality Is the Loop You Didn't Write
Re-embedding a knowledge base from one model into another is a data-migration job. In FloMorphic it's one node — because looping over a set is a property of the node, not a construct you build.
Every organisation that has been doing this for more than a year has vectors it can't quite explain. A knowledge base someone embedded in 2023 under a model that was state of the art that quarter. A product catalogue vectored by a service that has since been deprecated. The embeddings still work — until the day you want a better retriever, or a cheaper one, or one whose dimensions line up with the model your agents actually run now. Then you're stuck: the text is fine, the tags are fine, but the vectors were made by a model you've moved off, and there's no cheap way to change that. You have to re-embed — read every document out, run it through the new model, write it back — and while you're in there, quietly clean the tags nobody's maintained.
This is the least glamorous work in the whole stack, and it is the work that decides whether an agent has a brain worth reading. A retriever is only as good as the vectors under it. Migrating those vectors — from one model to another, from a messy collection to a clean one — is a real operation with a real shape, and the shape is: for each of a set of things, transform it and write it somewhere. Hold that sentence. It's the whole article.
I built a flow that does exactly this — re-embeds a Qdrant collection from an old model into gemini-embedding-2 — and the part worth showing isn't that it works. It's that the per-document transfer is one node, and the "for each" is not something I wrote.
First: the store the runtime never met
FloMorphic ships a built-in vector store, and for a lot of flows that's the right answer. But this knowledge base doesn't live there. It lives in our Qdrant, on our network, holding vectors from long before this flow existed. The runtime was never compiled against it and shouldn't have to be.
So the flow reaches it through the Qdrant plugin — and the plugin's most important property here is where it runs. It isn't baked into the FloMorphic or Inflowenger binary. It's installed the way a browser extension is installed, and it can sit anywhere it can reach the database — the same host, the same VPC, right next to a Qdrant that has been serving other systems for years. I installed it on our network, pointed it at our org's Qdrant, and from that moment the runtime knew three new actions — create a collection, upsert points, scroll points — without a single line of it being recompiled. That decoupling is the precondition for this whole job: you can't migrate a store the platform can't see, and a plugin is how the platform comes to see one it was never built for.
The credentials to touch that store live on the plugin node, scoped to it, never ambient to the flow. The graph you export and share — the recipe — carries none of them.
The whole flow is three nodes
Start → Create collection → Upsert points → (Scroll points)
Create collection calls qdrant.collection.create once and makes the target — selected_docs, size: 3072, distance: Cosine. That 3072 is the tell. Vector size isn't a stylistic choice; it's the dimensionality of the target model, gemini-embedding-2. The old collection's vectors are the wrong length for the new model, which is precisely why this is a migration and not a copy. The number that has to change is the one thing a plain export/import could never fix for you.
Upsert points is the node that does the work, and it's the reason to read this at all. Everything below is about it.
Scroll points is wired in but holds the expansion — the move from sample to whole collection. I'll come back to it.
One node, once per element
Here is the trick, and it isn't a trick. The upsert node has a scope:
scope: $.result.result[*]
$.result.result is the array of source points — the documents to migrate. The [*] makes the scope many-valued, and in FloMorphic a node's scope is its cardinality: a single-valued scope runs the node once; a many-valued scope runs it once per matched element. One upsert node on the canvas, one hundred documents in the array, one hundred writes to Qdrant. I did not draw a loop. I did not add a map step or a "for each" block. I told the node what set it ranges over, and the runtime ran it across that set.
This is the inversion worth internalising. In most tools, iterating is a thing you build — a loop node, a foreach, a fan-out operator you wire up and then reason about. In FloMorphic, iterating is a thing you declare: it's an attribute of the node, sitting right next to its title. The node doesn't know it's in a loop, because there is no loop. There's a node with a plural scope.
Inside each run, the node sees its own element as $this. That's what closes the loop you didn't write — a way to name the current one out of the set:
text: {{$this.payload.text}}
tags: component ← {{$this.payload.component}}
The first line takes the current point's stored text and hands it to the new embedding model; the plugin embeds it and writes the vector, keeping payload.text so the migrated point still carries its own source. The second line is the part I most want people to see. A payload tag, initialised at runtime from the element itself. The value {{$this.payload.component}} isn't known when I build the flow — it's resolved, per element, from whatever document is passing through right now. Every written point is shaped by the one it came from. Nothing is hardcoded, and nothing needed a code node to reshape it: the node's fields are templates, and inside a cardinal node those templates resolve against the current element.
That's the second lesson stacked on the first. Cardinality gives you "once per element." $this plus template fields give you "and each one configured by its data." Between them you have a transform — read a document, re-embed it, tag it from its own payload, write it — expressed as one node's configuration, not a program.
This isn't instead of a loop — it's the easy path when it fits
Be clear about what cardinality is and isn't. FloMorphic and the Inflowenger runtime treat the topology loop — a real backward edge over the durable Context — as the first-class way to iterate, and they mean it: it's the primitive the docs push you toward, the one that carries state across passes, branches, counts, and stops on a condition. The scroll pump later in this piece is that loop. Cardinality doesn't replace it and isn't in tension with it.
What a many-valued scope gives you is a shortcut for one specific, extremely common shape: apply the same transform to every element of a set, independently. No shared state between elements, no condition deciding when to stop, no branch — just "do this to each of them." When that's the job, drawing a topology loop with a counter and a backward edge is more machinery than the task deserves, and a scope with a [*] in it collapses the whole thing to one node. This migration is exactly that shape — each source point is embedded and written on its own, caring nothing about the others — which is why the array scope makes it so easy. Reach for the topology loop when the passes depend on each other; reach for cardinality when they don't.
The re-embed is the point, not the copy
It's worth being blunt about what happens inside that upsert, because "migration" can sound like moving bytes. It isn't. The old vector is discarded. The text goes through gemini-embedding-2 and a new 3072-dimension vector is computed. The payload comes across, optionally cleaned — I'm collapsing a couple of legacy tag shapes into a single component tag as the documents pass through, which is the "clean data on the way" that these jobs always turn out to need. What lands in selected_docs is genuinely new: same knowledge, different model, tidier tags. This is the operation you cannot avoid when you change embedding models, and here it's four fields on a node.
The expansion: a cursor and one backward edge
The sample migrates whatever set you hand it in $.result.result. Turning it into a migration of the entire source collection needs one more idea, and Qdrant hands it to us for free.
qdrant.points.scroll returns a next_page_offset — a cursor into the collection. The Scroll points node feeds that cursor straight back into its own next request:
offset: {{$.scrolled.result.next_page_offset}}
Read a page; the next run of the same node reads from exactly where the last one stopped. Now recall how loops work here: a loop is an edge you draw, not a node you configure. Wire a backward edge so the flow returns to the scroll after each page is transferred, and the two-node transfer becomes a pump: scroll a page, re-embed every point in it, scroll again from the cursor, until next_page_offset comes back empty and the collection is exhausted. The cardinality handles within a page; the cursor loop handles across pages. Together they walk an arbitrarily large store with a fixed, tiny graph.
I shipped the recipe with the transfer and the cursor in place and the backward edge left off — deliberately. The point of the sample is to make the cardinal node legible on its own. Closing the loop is the one line you add to go from "migrate these documents" to "migrate all of them," and it should be a line you draw knowingly, watching each page trace across the diagram.
What the three nodes are really teaching
Strip the Qdrant specifics and this flow is a general claim about how iteration should feel.
- Reaching a system is an install, not a build. The store predates the flow by years; a plugin installed next to it let the runtime operate on it without being recompiled or knowing it existed.
- Iterating over a set is a declaration, not a construction.
scope: $.result.result[*]is the entire "for each." The node runs once per element because you said what set it ranges over — no loop node, no map, no fan-out to reason about. - Each iteration configures itself from its own data.
$thisand{{...}}templates mean the current element shapes the write — text to embed, tags to attach — with no code node in the middle. - Scaling from a sample to the whole thing is one edge. A cursor the store already returns, plus a backward edge, and the transform becomes a sweep.
The unglamorous truth under all of it is that your agents are only as smart as the vectors they retrieve, and those vectors go stale — a model deprecates, a better one ships, a collection accretes junk tags. The organisations that keep their brain sharp are the ones for whom re-embedding and cleaning a knowledge base is cheap — cheap enough to do on a Tuesday, not a quarter-long project. Three nodes, a plugin, and a scope with a [*] in it is about as cheap as that gets.
Run it: the flow is in the Flow Cookbook — import
qdrant-migrate.json, install the Qdrant plugin next to your store, point the collections at yours, and watch one upsert node fire once per document.