Filtering
The builder filters fall into a few groups: status filters,
data-type filters, field-level filters (.where*()), document
targeting (.docId* / .mongoIds), the raw-query escape hatch,
policies, and scope escape hatches. All filters AND together — call
as many as you like, in any order.
Status filters
Status is a first-class concept on every document. You can filter by concrete value or by semantic category.
// Concrete status (OR within a single call)
.status('posted')
.status('draft', 'awaiting-approval')
// Semantic categories
.statusMutable() // all non-immutable statuses
.statusImmutable() // all immutable statuses
.statusFinal() // final success + final failure
.statusFinalSuccess() // final success only
.statusFinalFailure() // final failure only
.statusIntermediary() // non-final statuses only
.statusDefault() // the single default status
// Exclusion — chainable, call multiple times
.statusExclude('void')
.statusExclude('deleted')
Semantic categories are expanded at resolution time against the
document's StatusGroup, so the same query adapts automatically as
the definition evolves.
Soft-deleted documents
Soft-deleted documents (envelope status = 'deleted') are hidden
from every query by default. Opt-in to see them:
const rows = await bob.flowQuery('vendors/vendor')
.docId(vendorId)
.includeDeleted()
.getOne()
Data-type filters
Filter by the value stored in a data-type-bound field. Use the data-type slug — the resolver maps the slug to the concrete field path defined on the document.
.dataType('bill-payment-reconciliation-status', 'overdue')
.dataType('bill-payment-reconciliation-status', 'overdue', 'unpaid') // OR
.dataTypeExclude('bill-payment-reconciliation-status', 'paid')
Field-level filters
.where() takes a dot-notation field path, an operator, and a value.
Operators: eq, ne, gt, gte, lt, lte, contains,
startsWith, endsWith, in, nin, exists, notExists,
regex.
.where('data.amount', 'gt', 1000)
.where('data.vendorId', 'eq', 'V00001')
.where('data.tags', 'in', ['urgent', 'priority'])
.where('data.notes', 'contains', 'reconciliation')
.where('data.attachments', 'exists')
Multiple .where*() calls compose with AND. Contradictory filters
produce zero results, not an error.
Shorthand helpers
.whereIn('data.status', ['draft', 'posted'])
.whereNotIn('data.status', ['void', 'deleted'])
.whereExists('data.dueDate')
.whereNotExists('data.cancelledAt')
.whereContains('data.name', 'acme') // regex-based partial match
.whereBetween('data.amount', 100, 50_000) // inclusive: $gte + $lte
Raw query escape hatch
For anything the helpers don't cover, feed the compiler a native Mongo fragment:
.query({ docId: { $regex: '^B', $options: 'i' } })
.query({ 'data.metadata.internal': { $exists: true, $ne: null } })
The fragment is merged into the compiled query; it's an escape
hatch, not a replacement for the structured filters. Prefer
.where*() where possible so plugins, caches, and analytics see
the intent.
Document targeting
.docId('B00000048') // single
.docId('B00000048', 'B00000049') // multiple (varargs)
.docIds(['B00000048', 'B00000049']) // multiple (array)
.mongoIds(['6507a1b2c3d4e5f6a7b8c9d0']) // target by _id
.extraDocumentIds(['AUX_ID_1', 'AUX_ID_2']) // additional ids
docIds([]) deliberately returns zero results — an empty allow-list
is not the same as "no filter."
Policies
Apply a named document policy to the query. Policies are defined on
the document and enforce fine-grained access rules (e.g. canUpdate,
canViewPII):
.addPolicy('canUpdate')
.addPolicy('canViewPII')
Scope escape hatches
Every query is implicitly scoped by the calling user's membership, company, and business unit. These escape hatches lift the scope when you need cross-tenant or system-level reads:
.skipMembership() // bypass membership scoping
.skipCompany() // bypass company scoping
.skipBusinessUnit() // bypass business unit scoping
.setCompanyId('COMP-42') // pin a specific company
.setSessionTransactionId(id) // pin a specific session
.skipSession() // bypass the debe transaction entirely
Use these deliberately. The default scoping is what prevents one tenant from seeing another's data.
Putting it together
const claims = await bob.flowQuery('finance/expense-claim')
.statusMutable()
.dataType('expense-category', 'travel', 'meals')
.where('data.amount', 'gt', 25)
.whereExists('data.receiptUrl')
.addPolicy('canApprove')
.sortDesc('data.submittedAt')
.limit(100)
.getMany('FinanceInterface.ExpenseClaim')
Next: References →