Review
For every blend, blendx writes a file that says what the API does, in terms a person can check without reading TypeScript: review/<table>.yaml. Beside it, review/<table>.examples.yaml holds concrete cases that person expects to hold. Together they let a product owner, an auditor, or whoever reviews an agent's work check the behaviour, and ask for a change.
The format is specified in packages/spec/review-format.md. This page shows how to use it.
review/<table>.yaml
bunx blendx review writes one file per blend. The file is generated, and the same code always gives the same file. An excerpt from examples/expenses:
# Generated by `blendx review`. Do not edit: a change here is a fix request.
# Change the blend until `blendx review --check` passes.
format: 1
resource: expenses
source: blends/expenses.ts
record:
id: integer
user_id: integer
description: string, at most 200 characters
category: one of travel, meals, office, other
amount: string
tax: string
total: string
status: one of draft, submitted, approved, rejected
...
actions:
...
reject:
route: POST /expenses/:id/reject
input:
note: string, at least 3 characters, at most 500 characters
stages:
rules: nothing (an empty object) # from: schema, action
load: the row by id, not soft-deleted, locked for update
authorize: an approver # from: schema, action
calculate: the input's writable columns # from: schema, action
save: update the row, touching updated_at, when there is something to write
respond: 200 with the saved record
calculate:
source: |-
({ input }) => ({ status: 'rejected' as const, review_note: input.note })
writes: [review_note, status]
reply:
status: 200
body: the record
How to read it:
recordis what a reply carries for one row, hidden columns already removed.- Each action has its
route, theinputit accepts (one line per field, from the resolved rules), and one line per pipeline stage saying what the default does. A string with a format reads as that format, such asstring (date)orstring (email). # from: schema, actionmarks a stage that a hook changed: here the action's own rules, authorize and calculate replaced or extended the schema's defaults. A stage without the comment does exactly what its line says.- The authorize line is the policy's description (
an approver,owner (user_id = auth.id)). An authorize hook on top of it shows as# from: schema, action; its condition is in the blend. - An action with an
afterhook listsafter: nothing # from: schema, actionbetween save and respond: something happens once its write has committed, such as an email, and the comment says which levels' hooks run (Hooks). Actions without one have no after line. calculateshows the hook exactly as written, and the columns itwrites(for a collection action, the keys itreturns).revealslists the hidden columns an action's reply carries, and its reply then readsthe record, with api_token. It is where to check that a secret leaves the server only where it should.replyis the status and the body.
Examples
review/<table>.examples.yaml belongs to the people who review. It maps action names to lists of examples: an input (and a record, for a member action), and then what must come out of it:
writes: the columns calculate must return;returns: the same, for a collection action;rejects: the fields validation must refuse: JSON pointers (/amount) for a body, parameter names (category) for agetaction's query.
From examples/expenses:
store:
- name: meals carry 10% tax
input: { description: Team lunch, category: meals, amount: '40.00', spent_on: '2026-09-01' }
writes:
{ description: Team lunch, category: meals, amount: '40.00', spent_on: '2026-09-01', tax: '4.00', total: '44.00' }
- name: the claimant and the totals are never sent
input: { description: Taxi, category: travel, amount: '12.50', spent_on: '2026-09-04', user_id: 2, total: '1.00' }
rejects: [/total, /user_id]
update:
- name: a new amount is priced with the claim's category
record: { category: office, amount: '10.00' }
input: { amount: '20.00' }
writes: { amount: '20.00', tax: '4.00', total: '24.00' }
quote:
- input: { amount: '40.00', category: meals }
returns: { tax: '4.00', total: '44.00' }
Examples run without a database, the way the engine runs the action: the resolved rules validate the input, then calculate runs. Policies, authorize hooks and saving do not run, so examples pin down validation and calculation; permissions are read in the review file and tested in the app's tests. blendx review --check runs every example, and so can the app's tests (Testing).
Asking for a change
Editing a review file does not change the API. It is how a reviewer asks for a change:
- The reviewer edits
review/<table>.yamlto say what should be different, or adds an example with the output they expect. bunx blendx review --checknow fails. An edited line shows as a unified diff, where the-lines are what the reviewer wrote and the+lines what the code does. A new example fails with one line naming the file, the action, the example, and what came out against what was expected.- A developer or an agent changes the blend (a policy, a rule, a hook or a calculate), never the YAML, and runs
bunx blendx generate. - When the blend does what the reviewer wrote,
blendx review --checkpasses and the reviewer's edit stays as it is. If the change also moved lines the reviewer did not touch, such as a calculate's source,bunx blendx reviewrewrites the file.
For example, a reviewer who wants approvers to be able to delete claims changes destroy's line from authorize: owner (user_id = auth.id) # from: schema, action to authorize: an approver # from: schema, action. The check fails with that line in its diff until destroy gets the approvers policy in the blend.
Run bunx blendx review --check in CI next to bunx blendx generate --check: together they fail when the code and what was reviewed have drifted apart, in either direction.
Who does what
| Writes | Reads | |
|---|---|---|
| Developer or agent | schema.dbml, blends, src/app.ts, tests | the diffs of a failing review --check |
blendx review | review/<table>.yaml | the blends |
| Reviewer | edits to review/<table>.yaml, review/<table>.examples.yaml | review/*.yaml |