Flow Query
bob.flowQuery() is the primary interface for reading and writing
documents in Bob. It's a fluent, chainable builder that speaks in terms
of documents, statuses, and data types — not collections, filters, and
raw queries — and it compiles down to whichever database backend is
active for the request.
Every call follows the same path:
Build → Resolve → Plugins → Compile → Execute
The builder accumulates your intent into a serializable Intermediate Representation (IR). When a terminal method is called the IR is hydrated with definitions from the registry, optionally transformed by plugins, compiled to a native query for the active backend (MongoDB by default, with PostgreSQL and ClickHouse available), and executed against the database with policy and immutability guards applied.
You never construct a FlowQueryBuilder directly. Inside any hook,
bob.flowQuery(documentPath) is always how you start.
First example
const bills = await bob.flowQuery('finance/bill')
.statusMutable()
.dataType('bill-payment-reconciliation-status', 'overdue')
.withView('get-vendor')
.limit(50)
.getMany('FinanceInterface.Bill')
bills.data // FlowDocument<FinanceInterface.Bill>[]
bills.meta // { totalDocuments: 142, queryTimeMs: 17 }
This reads every "mutable" bill whose
bill-payment-reconciliation-status data-type is overdue, joins the
vendor document in via the get-vendor view, caps the result at 50
rows, and casts the returned documents to FinanceInterface.Bill.
Concepts
Document path. Every query targets a document path of the form
namespace/document, e.g. finance/bill or contacts/contact. The
path is looked up once against the FlowDefinitionRegistry during
resolution. If the path is unknown the query throws a
FlowDocumentNotFoundError.
Statuses. Each document defines a set of statuses — some
intermediary (draft, pending), some final (posted, void),
some immutable (posted bills cannot be edited without opting in).
Flow Query lets you filter by concrete status ('posted') or by
semantic category (.statusMutable(), .statusFinal()). The same
vocabulary drives transitions: .setStatus('posted').
Data types. A data type is a named enumeration of values attached
to a field — for example, the shipping-status data type attached to
a sales order's shippingStatusDataTypeId field. Data types are
filterable (.dataType('shipping-status', 'shipped')) and
transitionable (.setDataType('shipping-status', 'shipped')).
Views. Views are named lookup pipelines stored on the document
definition. .withView('get-vendor') adds the pipeline to the
compiled query so the result already contains the joined vendor. No
manual $lookup needed.
Session. Every hook runs inside a Bob debe session — a
transaction that commits on success and rolls back on failure. All
reads and writes inside a session see each other; nothing is visible
externally until the session commits. .skipSession() is the escape
hatch for writes that must persist regardless of hook outcome.
Soft delete. .deleteOne() and .deleteMany() flip the envelope
status to deleted. The document is hidden from every subsequent
query by default; .includeDeleted() reveals it.
Pipeline
bob.flowQuery('finance/bill').statusMutable().limit(50).getMany()
│
▼
┌────────────────────┐
│ BUILD │ — FlowQueryBuilder accumulates intent into IR.
│ │ No validation, no definition lookup.
└─────────┬──────────┘
▼
┌────────────────────┐
│ RESOLVE │ — Registry.get(documentPath) hydrates
│ │ document identity, expands semantic
│ │ statuses, resolves data-type slugs to
│ │ field paths, matches views, merges
│ │ membership / business unit scoping.
└─────────┬──────────┘
▼
┌────────────────────┐
│ PLUGINS (pre) │ — plugin.transformIR(resolved, config)
└─────────┬──────────┘
▼
┌────────────────────┐
│ COMPILE │ — MongoCompiler / PostgresCompiler /
│ │ ClickHouseCompiler produces native
│ │ query + options.
└─────────┬──────────┘
▼
┌────────────────────┐
│ PLUGINS (post) │ — plugin.postCompile(native, resolved, …)
└─────────┬──────────┘
▼
┌────────────────────┐
│ EXECUTE │ — Executor runs the query, enforces
│ │ policies, checks immutability on writes,
│ │ type-casts the result.
└─────────┬──────────┘
▼
┌────────────────────┐
│ PLUGINS (post-run) │ — plugin.postExecute(result, resolved, …)
└────────────────────┘
│
▼
SearchResponse | CreateResponse | UpdateResponse | DeleteResponse
Default backend and provider override
MongoDB is the default backend. Pass { provider } to route a query
to a different one — typically ClickHouse for analytics pipelines:
const totals = await bob.flowQuery('finance/bill', { provider: 'clickhouse' })
.statusFinal()
.aggregate([
{ $group: { _id: '$data.vendorId', total: { $sum: '$data.amount' } } },
])
.getMany()
The same fluent chain applies across backends. When something isn't supported on the target backend the compiler throws at compile time, not at runtime.
Where to next
The rest of this guide walks the builder, surface by surface. If you're new to Flow Query, read them in order; if you're looking for a specific method, jump straight in.
- Filtering —
where,whereIn,docId,status,dataType, policies, scope escape hatches - References —
statusRef()andfieldDataType()/slugDataType()for synchronous value lookups in write payloads - Reading — every read terminal:
getMany,getOne,getById,paginate,pluck,distinct, … - Writing —
insert,update,patch,setStatus,setDataType,delete - Mutations — the
.mutate()chain for atomic field-level operators (set,inc,push,pull, …) - Bulk — two-phase batching of DB operations and hooks
- Aggregations —
aggregate,lookup,addFields - Plugins —
.use()and the three plugin hooks - Advanced —
toIR,plan,clone,watch,skipValidation,includeDeleted, caller overrides - Migration from legacy — side-by-side
mapping from
@logic-bee/flow-query
Next: Filtering →