blendx docs

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:
        default: an approver
        limitation: Custom hooks can change this policy decision.
        hooks:
          action:
            file: blends/expenses.ts
            source: |-
              ({ prev, auth, record }) => prev && reviewable(record, auth)
      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:

  • record is what a reply carries for one row, hidden columns already removed.
  • Each action has its route, the input it 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 as string (date) or string (email).
  • # from: schema, action marks an unchanged scalar stage that a hook changed: here the action's own rules and calculate replaced or extended the schema's defaults. A stage without the comment does exactly what its line says.
  • A custom authorize or save stage is a block. Its default is the policy or schema behavior that starts the hook, limitation says why that line does not describe the hook, and hooks gives every custom level's file and readable source. A referenced, imported or spread hook retains its file and says to write the hook inline before review can show it.
  • An action with an after hook lists after: nothing # from: schema, action between 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. A later hook shows the same way, as later: nothing # from: ... just before after: an effect the outbox worker runs, at least once (Hooks).
  • calculate shows the hook exactly as written, and the columns it writes (for a collection action, the keys it returns).
  • reveals lists the hidden columns an action's reply carries, and its reply then reads the record, with api_token. It is where to check that a secret leaves the server only where it should.
  • reply is 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 a get action'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:

  1. The reviewer edits review/<table>.yaml to say what should be different, or adds an example with the output they expect.
  2. bunx blendx review --check now 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.
  3. 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.
  4. When the blend does what the reviewer wrote, blendx review --check passes 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 review rewrites 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

WritesReads
Developer or agentschema.dbml, blends, src/app.ts, teststhe diffs of a failing review --check
blendx reviewreview/<table>.yamlthe blends
Revieweredits to review/<table>.yaml, review/<table>.examples.yamlreview/*.yaml