Skip to main content

Writing documents

Flow Query covers the full write lifecycle: insert, update (full-replace), patch (partial), status transition, data-type transition, and delete. All writes participate in the current debe session and are rolled back automatically if the hook fails.

Schema validation (Joi) runs by default on every write. See Validation for the control surface.

insertOne / insertMany​

const result = await bob.flowQuery('vendors/vendor')
.insertOne({
id: 'vendor-001',
name: 'Acme Corp',
legalEntityName: 'Acme Corp LLC',
email: 'contact@acme.com',
})

result.ok // true
result.docIds // ['VJxyz…']
result.data // inserted document(s)

// Many documents
await bob.flowQuery('finance/bill').insertMany([doc1, doc2, doc3])

Rules:

  • Missing required fields → throws bad_validation_request.
  • Extra non-schema fields → silently stripped on insert.
  • Type coercion is applied (e.g. '5' → 5 for Joi.number()).
  • Defaults from the schema are auto-applied.
  • Envelope fields (docId, info.createdAt, info.createdBy, etc.) are populated from context.

Use .insert() (no terminal) when you want to prepare an insert for a bulk — see Bulk.

updateOne / updateMany — full replace​

Full replace of the document data. The payload must contain every required field.

await bob.flowQuery('vendors/vendor')
.docId(docId)
.updateOne({
id: 'vendor-001',
name: 'Acme Corp',
legalEntityName: 'Acme Corp LLC',
email: 'updated@acme.com',
status: 'active',
})

// Update all matching documents
await bob.flowQuery('vendors/vendor')
.where('data.archived', 'eq', true)
.updateMany({ /* full replacement payload */ })

Missing required fields → throws. If you want to touch only a subset, use patchOne / patchMany or .mutate().

Modify immutable documents​

Documents in an immutable status (e.g. posted) reject ordinary updates. Opt in explicitly when the hook has the authority to do so:

await bob.flowQuery('finance/bill')
.docId(docId)
.updateOne(payload, { modifyImmutable: true })

patchOne / patchMany — partial update​

Merge-style update — only touches the fields you pass. Unknown fields are rejected (unlike insert, which strips them).

await bob.flowQuery('vendors/vendor')
.docId(docId)
.patchOne({ phoneNumber: '555-9999' })

await bob.flowQuery('vendors/vendor')
.where('data.name', 'eq', name)
.patchMany({ standingStatus: 'preferred' })

Patch behaviour:

BehaviourInsertPatch
Missing required fieldsthrowsok (not touched)
Extra unknown fieldsstrippedthrows
Type coercionyesyes
Null for optional fieldsallowedallowed

Patching a non-existent docId is not an error — ok: false / modifiedCount: 0 is returned. Check the return value if you care.

setStatus — status transition​

await bob.flowQuery('finance/bill')
.docId(docId)
.setStatus('posted')

setStatus enforces the status transition rules defined on the document and runs the same pre/post hooks a write would. To avoid hardcoded strings, resolve the target with statusRef:

const posted = bob.flowQuery('finance/bill').statusRef().value('posted')

await bob.flowQuery('finance/bill').docId(docId).setStatus(posted)

See References for the full statusRef API.

setDataType — data-type transition​

await bob.flowQuery('finance/bill')
.docId(docId)
.setDataType('bill-payment-reconciliation-status', 'paid')

Same pattern as setStatus — the target value can be resolved via fieldDataType or slugDataType to avoid magic strings.

deleteOne / deleteMany — soft delete​

Soft delete: the envelope status is flipped to 'deleted', the document is hidden from every subsequent query by default, but the document's data is preserved intact.

const result = await bob.flowQuery('vendors/vendor')
.docId(docId)
.deleteOne()

result.ok // true
result.deleteCount // 1

// Bulk soft delete
await bob.flowQuery('vendors/vendor')
.where('data.name', 'contains', '__TEST_')
.limit(1000)
.deleteMany()

.includeDeleted() reveals soft-deleted documents:

const row = await bob.flowQuery('vendors/vendor')
.docId(deletedDocId)
.includeDeleted()
.getOne()

row.status // → 'deleted' (envelope)
row.data.status // → 'active' (business — unchanged)

Use .delete() (no terminal) to pre-configure a delete for bulk execution.

Validation​

Writes are validated against the document's Joi schema by default. Bypass the schema with .skipValidation() for migrations, system writes, or pre-validated payloads:

await bob.flowQuery('audit/log')
.skipValidation()
.insertOne(rawData)

Two helpers let you validate without writing — useful for UI pre-flighting:

await bob.flowQuery('vendors/vendor').validateData(payload)
// → { ok: boolean, errors: [...] }

await bob.flowQuery('vendors/vendor').validateDataOrThrow(payload)
// → throws on failure, returns void on success

Helpers​

// Produce a fully-defaulted empty payload for a document
const empty = await bob.flowQuery('vendors/vendor').generateEmptyDocument()

// Enforce the document kind at call site
bob.flowQuery('finance/settings').isConfigDocumentOrThrow()
bob.flowQuery('finance/settings').isSettingsOrThrow()

Envelope overrides​

Out-of-the-ordinary writes sometimes need to set fields that are normally managed by the framework. Three overrides are available:

.docIdOverride('CUSTOM-DOC-ID')         // custom docId on insert
.overrideCreatedAt(new Date('2024-01-01'))
.overrideUpdatedAt(new Date('2024-02-01'))

Use these sparingly — they exist for migration scripts and data backfills.

Tracking metadata​

Every mutation updates the envelope metadata automatically:

doc.info.createdAt  // set on insert
doc.info.createdBy // set from FlowUser
doc.info.updatedAt // set on every update / patch
doc.info.updatedBy // set from FlowUser

No need to write these explicitly.

Session and transaction behaviour​

All writes run inside the hook's debe session by default. Reads within the same session see the uncommitted writes; nothing escapes to other sessions until the hook commits successfully.

.skipSession() commits immediately to the database and cannot be rolled back by the session. Use it for writes that must survive a hook-level failure (audit logs, metrics) — never for business data.

Next: Mutations →