Skip to main content

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.

  • Filteringwhere, whereIn, docId, status, dataType, policies, scope escape hatches
  • ReferencesstatusRef() and fieldDataType() / slugDataType() for synchronous value lookups in write payloads
  • Reading — every read terminal: getMany, getOne, getById, paginate, pluck, distinct, …
  • Writinginsert, 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
  • Aggregationsaggregate, lookup, addFields
  • Plugins.use() and the three plugin hooks
  • AdvancedtoIR, plan, clone, watch, skipValidation, includeDeleted, caller overrides
  • Migration from legacy — side-by-side mapping from @logic-bee/flow-query

Next: Filtering →