BuildWork

Documentation

The builder, explained

What each palette field does, how the config drawer shapes it, and what a workflow can enforce as an order moves. Everything on this page runs in the playground — no account needed to try it.

Open the playground

Guided chapters

Each chapter opens the builder in the playground and walks you through one part of it. Nothing you do there is saved to your company, and you can re-run a chapter as often as you like.

The palette

Thirteen field and layout types in four groups, ordered the way a template gets built: the plain values a form collects, the choices that constrain an answer to a set, the fields that reach outside the form, and the containers that arrange it all. Drag one onto the canvas and it lands at its default size — authored for the default 12-column grid, scaled to your template’s column count.

Values

  • Text field3×1

    One line of free text — a reference, a name, a note that fits on a line.

  • Text area3×2

    Multi-line text for instructions and longer notes.

  • Number field3×1

    A numeric value, with decimal places if you want them.

  • Date field3×1

    A calendar date.

Choices

  • Select field3×1

    One choice from a dropdown you define — or several at once when multiple selection is on.

  • Checkbox2×1

    A single yes-or-no tick.

  • Switch3×1

    The same yes-or-no drawn as a toggle — for states rather than confirmations.

  • Radio3×1

    One choice with every option visible at once.

Linked

  • Material3×1

    Points at a material in your stock. Bind it to a number field and the workflow can deduct that amount when an order advances.

  • Formula3×1

    A number computed from other fields — a number field dropped with its formula already open.

Layout

  • Group6×2

    A repeating set of sub-fields: line items. Every order row repeats the whole set.

  • Card12×3

    A titled container that frames related fields and can show or hide all of them with one rule.

  • Divider12×1

    A horizontal rule to separate sections.

Configuring a field

Click a field and its drawer opens beside the canvas. There is no save button — every change applies as you make it, and Ctrl+Z takes it back.

General

Label and help text on every field, plus the type’s own settings: decimal places on a number, the options list on a select or radio, the deduct-from binding on a material.

Each field gets a key derived from its label — the name formulas and visibility rules use. A key freezes once the template is published or another field references it, so nothing that depends on it can silently break.

Validation

Any field can be required. Beyond that: a maximum length on text, a minimum and maximum on numbers, and a row minimum and maximum on repeating groups.

Logic

Computed values

A number field can calculate itself from other fields, using arithmetic and a small set of rounding and min/max functions. References must point at other number fields — except in lookup, which reads a property of the material a reference field points to — and cycles are rejected as you type.

qty * unit_price

Conditional visibility

Any field — or a whole card — can appear only while a rule holds. A hidden field is stripped from the submitted order, not merely hidden on screen.

coating == "matte" and qty > 0

Per-row visibility

A field inside a repeating group can carry its own rule, checked separately in each row — the same column can show in one row and hide in the next. It may name the row’s other fields by key, plus any field of an enclosing group or of the form itself, and group quantifiers work here too. A cell hidden in a row is stripped from that row of the submitted order and does not print.

kind == "custom" and qty > 0

All functions and operators

Functions and operators

Two small languages appear in the builder. Formulas compute a number. Conditions answer yes or no — they drive field and card visibility and workflow advance guards. Both are plain text with a fixed vocabulary, and anything outside it is rejected as you type.

In formulas

+ - * / % ^
Add, subtract, multiply, divide, remainder and power. Parentheses group as usual.
abs(x)
The value without its sign — abs(-3) is 3.
ceil(x)
Rounds up to the next whole number — ceil(2.1) is 3.
floor(x)
Rounds down to the previous whole number — floor(2.9) is 2.
round(x, digits)
Rounds to the nearest whole number — or to a number of decimal places: round(2.345, 2) is 2.35.
min(a, b, …)
The smallest of any number of values.
max(a, b, …)
The largest of any number of values.

How formulas behave

  • References are field keys and must point at other number fields. Inside a repeating group, a row also sees the template’s top-level number fields.
  • A missing, empty or non-numeric input makes the result empty — never zero — and an empty result carries through formulas that build on it.
  • Division by zero gives an empty result rather than an error.
  • The final value rounds to the field’s decimal places.
  • Unknown references and circular formulas are rejected as you type; a formula caps at 1024 characters.

Totals over repeating groups

A formula can also reduce a repeating group’s rows to a single number. Name the group, then the sub-field column — over a line-items group items with a number sub-field cost, an order total is:

order_total = sum(items, cost)

The total adds up the cost of every row and updates as rows are added and filled in.

sum(group, field)
The total of a number column over the group’s rows.
avg(group, field)
The average of the filled cells in a number column.
min(group, field)
The smallest value in a number column — the same min, aggregating when its first argument is a group.
max(group, field)
The largest value in a number column, the same way.
count(group)
How many rows the group has.
countFilled(group, field)
How many rows have answered that column.
defined(group, field)
1 when every row has answered that column, 0 otherwise.
allChecked(group, field)
1 when a checkbox or switch column is ticked in every row, 0 otherwise. The one aggregation that asks about ticks — defined cannot, because an untouched tick box already counts as answered.
sum(group.nested, field)
Every aggregation also takes a dotted path into nested groups: sum(items.item, cost) totals the cost column of every item row across all items rows, flattened into one set.

How aggregation behaves

  • Blank or non-numeric cells are skipped, not counted as zero — a total stays partial and grows as rows are filled in. Zero and negative values do count.
  • countFilled and defined ask whether a cell is answered: empty text and an empty selection are not answers, while an unticked checkbox (false) and the number 0 are. So defined over a checkbox column is always 1 — allChecked is the one that counts ticks, and it only accepts a checkbox or switch column.
  • Aggregation starts at the direct child groups of the formula’s own scope and reaches deeper with dots: sum(items.item, cost) gathers every item row of every items row into one flat set. A row formula can do the same over the row’s own nested groups — never over the group it sits in. A row that is missing the nested list simply contributes nothing to the total.
  • Over an empty group, sum, count and countFilled give 0; avg, min and max give an empty result; defined and allChecked give 1 — with no rows, nothing is left unanswered or unticked.

Reading the selected material

A formula can also read the material a reference field points to. Name the reference field, then the property — in a line-items group with a material reference mat and a quantity qty, a row’s cost is:

rowcost = qty * lookup(mat, price)

The row cost multiplies the quantity by the selected material’s price per unit, and follows the row when a different material is picked.

lookup(field, property)
The named property of the material a reference field points to — lookup(mat, price) is the per-unit price of the row’s selected material.

How lookup behaves

  • The reference must be a material reference in the formula’s own scope or any enclosing scope: a row formula can read its own row’s reference, its parent row’s, or a top-level one. Lookup never reaches down into a group’s rows — a row has one enclosing material, but a group has many rows — and there are no dotted paths.
  • One property exists today: price — the material’s price per unit, as entered in your stock.
  • Until a material is selected, the lookup is empty — and the empty result carries through formulas that build on it, as formula results always do.
  • Values are captured when the order is written: an order keeps the price that held when its rows were saved, and a later price change in your stock never silently rewrites an existing order.
  • lookup cannot be an aggregation column — sum(items, lookup(mat, price)) is rejected. Compute per row instead and total at the top: rowcost = qty * lookup(mat, price) in the row, order_total = sum(items, rowcost) above it.

In conditions

== !=
Equal and not equal. Exact match, no type bending — the number 1 is not the text "1".
> >= < <=
Number comparisons. False whenever either side is not a number.
contains(field, value)
On a multi-select, true when that choice is picked; on text, true when the text contains the fragment.
isEmpty(field)
True for empty text or when nothing is selected. An unchecked checkbox and the number 0 are not empty.
isNotEmpty(field)
The opposite of isEmpty.
and or
Chain conditions with and, or with or — one rule uses one of them, never both.

How conditions behave

  • The field key goes on the left, the value on the right: a number, text in double quotes, or true and false. Comparing two fields to each other is not supported.
  • A condition over a missing value is false, so a show-rule hides its field — rules fail towards hiding, never towards showing by accident.
  • The same conditions power field visibility, card visibility, per-row visibility of a field inside a repeating group and workflow advance guards.

Conditions over repeating groups

A condition can also ask about a repeating group as a whole — how many rows it has, whether every row has answered a column, or whether every row has ticked one. Name the group — or a dotted path into its nested groups — then the column. These are the four forms:

defined(items, mat)allChecked(items, done)count(items) > 0countFilled(items, note) >= 2

defined(group, field)
True when that column is answered in every row.
allChecked(group, field)
True when a checkbox or switch column is ticked in every row.
count(group) > 0
How many rows the group has, compared against a plain number. Every comparison operator works: == != > >= < <=.
countFilled(group, field) >= 2
How many rows have answered that column, compared against a plain number.

How group conditions behave

  • defined(group, field) is true when that field is filled in every row — and also when the group has no rows at all. To require rows as well, write count(group) > 0 and defined(group, field). It is the same vacuous truth the formula defined has over an empty group, and it is the one caveat that quietly lets an empty order through a gate. It holds through a dotted path too: a row whose nested list is empty leaves defined true. allChecked behaves the same way — pair it with count(group) > 0 for the same reason.
  • Answered means the same here as in the aggregation functions: empty text and an empty selection are not answers, while an unchecked checkbox (false) and the number 0 are. This is why a tick box needs allChecked: defined over a checkbox column is true the moment the row exists. allChecked only accepts a checkbox or switch column — on anything else it could never be true, so the builder refuses it rather than letting the rule sit there dead.
  • A group condition names a top-level repeating group and reaches its nested groups with dots: defined(items.item, mat) asks about every item row of every items row, flattened into one set — count(items.item) > 0 counts them all.
  • If the group is not there at all — hidden by a card rule, or never filled in — every condition about it is false, including count(group) == 0. The same holds at depth: if any row is missing its nested list entirely, the condition is false — a gate never opens on malformed data. Formulas differ here on purpose: an aggregation quietly skips such a row, a condition refuses.
  • All four forms work everywhere a condition does: field visibility, card visibility, per-row visibility of a field inside a repeating group and workflow advance guards.

Worked example: never deduct on a half-filled order

A line-items group items holds a material reference and a quantity column qty bound to stock deduction. On the stage before the one that deducts, set the advance condition to:

count(items) > 0 and defined(items, qty)

An order with no rows cannot advance, and neither can one whose rows still have a blank quantity — so stock never leaves for an order nobody finished filling in.

This gate is the moment to get it right: once an order has passed the deduction stage, the amounts it deducted are locked. Editing them is refused, because that stock has already left the shelf.

Canvas rules

  • Each template picks its own column count — 3 to 20, default 12 — from the Grid columns control beside the surface toggle. Drag a field’s edge to set how many columns it spans, drag its body to move it — fields never overlap.
  • Cards and groups nest up to five containers deep — enough to structure a form, not enough to get lost in one.
  • Repeating groups go at most two levels: line items can hold line items, and no further.
  • Keys are unique within their scope. Two different groups may reuse a sub-field key, but a sub-field never shadows a field above it.
Practise this in the builder:Repeating groups and column sums

Workflow

Every template carries its own ordered list of stages — there is no fixed status list to fit your process into. The list is the workflow: the first stage is where orders start, the last is where they finish, and orders move one stage at a time, forward or back.

A label shop’s preset, for the shape of it:

  1. New
  2. Printingdeducts stock
  3. Done
  4. Invoicedinvoice no.

What a workflow can enforce

  • An advance condition guards a step: the order cannot move forward until its values satisfy the rule. defects == 0 and approved == true
  • Each stage either counts as active work or not — that is what feeds the shop-floor board, so finished stages can drop off it.
  • One stage may require an invoice number as an order moves into it; the number is stored on the order.
  • One stage may notify the company's notification recipients that an invoice is needed when an order moves into it — in the app's bell, to the members switched on under Settings → Members. It does not block the move.
  • One stage may deduct stock. Advancing into it deducts every bound material amount — all of it or none of it, and short stock blocks the move.
  • Moving back is always allowed, and a deduction never happens twice.

All of it is enforced on the server against the template version the order was created from — the buttons in the interface are a convenience, not the gate.

Publishing and versions

A draft becomes orderable by publishing a version. Versions are immutable: new orders take the newest one, existing orders keep the version they were created with. Editing the template tomorrow never rewrites yesterday’s job.

Floor board

The floor board shows one template’s active orders as cards in stage columns. A saved view is a shared configuration of that board: an owner or admin creates it, and everyone with view access loads it from the switcher and sees the same board. Besides its name and the “Mine” toggle, a view carries two filters and a list of card fields.

Two filters, applied on the server

  • A rule is a condition over an order’s top-level values — the same grammar as visibility rules, quantifiers over repeating groups included, so count(items) > 0 works here too; see “Functions and operators” above. The server evaluates it against each order’s own published version: an order whose version does not have a field the rule names is hidden, not shown by accident.
  • An assignee selector keeps only orders assigned to the chosen members, to nobody (“Unassigned”), or either. Nothing selected means everyone. The “Mine” toggle composes on top for whoever is looking — it narrows the view’s result further, it never replaces it.

Card fields

A view can list fields to show on every card as rows under the title, in the order picked. Only top-level fields qualify — a field inside a group cannot be a row — and a reference field shows the referenced record’s name, not its id. A card shows at most 8 fields. A field the order’s own version does not have renders nothing; the rest of the card is unaffected.

The settings live behind the gear beside Save and Update on the board and apply to the active saved view. Opening them needs the permission to update views — owners and admins by default. Floor roles still pick saved views and use “Mine”; they see the board the view defines, read-only.

Views saved before these settings existed keep working unchanged: no rule, no assignee filter, no card fields — the whole board, exactly as before. Open the gear on one to add them.

Build one against your own paperwork

The playground runs the real builder with sample clients and materials. Nothing is saved, and no account is needed.

Open the playground