Grids
Summary
A Grid is a full-screen editable table — used for payment journals, batch entry, price lists, and similar tabular workflows. Unlike HeaderLines (which overlays a grid on a card), Grid renders the table as the entire screen.
What the client does with a Grid
The client renders a title bar, a full-screen editable grid, an optional totals footer, and a bottom bar with keyboard hints. The user edits cells directly — no card, no Ctrl+L overlay toggle. Escape goes straight back to the previous screen.
┌─ Banking Journal ────────────────────────────────────────────────────────┐
│ Posting Date Document Type Document No. Acc. Type Account No. Desc… │
│ ─────────── ───────────── ──────────── ───────── ─────────── ─────── │
│ 16-03-2026 Payment PAY-001 Customer 10000 Acme… │
│ 16-03-2026 Invoice INV-100 Customer 20000 Sela… │
│ │
│ │
│ Balance: 1,200.00 │
├──────────────────────────────────────────────────────────────────────┤
│ Ctrl+S Save F3 Insert F4 Delete Ctrl+A Actions Esc Close │
└──────────────────────────────────────────────────────────────────────┘Building a Grid screen
Route
GET /screen/journal → ScreenContractResponse structure
json
{
"layout": "Grid",
"title": "Banking Journal",
"sections": [],
"lines": {
"columns": [
{
"id": "posting_date",
"label": "Posting Date",
"type": "Date",
"width": 12,
"editable": true
},
{
"id": "document_type",
"label": "Document Type",
"type": "Option",
"width": 14,
"editable": true,
"options": [
"",
"Payment",
"Invoice",
"Credit Memo"
]
},
{
"id": "account_no",
"label": "Account No.",
"type": "Text",
"width": 12,
"editable": true,
"lookup": {
"endpoint": "/lookup/account_no",
"validate": "/validate/account_no"
}
},
{
"id": "description",
"label": "Description",
"type": "Text",
"width": "fill",
"editable": true
},
{
"id": "amount",
"label": "Amount",
"type": "Decimal",
"width": 14,
"align": "right",
"editable": true,
"validation": {
"decimals": 2
}
}
],
"rows": [],
"row_count": 0,
"editable": true,
"selectable": true
},
"totals": [
{
"label": "Starting Balance:",
"value": "0.00"
},
{
"label": "Balance:",
"value": "0.00",
"source_column": "amount"
}
],
"actions": {
"save": "/screen/journal/save"
},
"screen_actions": [
{
"id": "post_journal",
"label": "Post Journal",
"kind": "confirm",
"endpoint": "/action/journal/post",
"confirm_message": "Post all journal lines? This cannot be undone."
}
]
}Key things to notice
layout: "Grid"— this is not HeaderLines. No card, no overlay.sectionsis empty — Grid screens have no header fields.- All columns are
editable: true— the entire grid is for data entry. totals— server-computed summary values displayed in a footer row.screen_actions— posting is a server-defined action, triggered viaCtrl+A. See Screen Actions.- No
createordeleteaction — journal lines are managed within the grid (Ctrl+Ninserts,Ctrl+Ddeletes rows). The screen itself is not a record.
Editable grid columns
Grid columns use the same ColumnDef structure as HeaderLines grids. All column types work: Text, Date, Decimal, Option. Lookups and validation work the same way.
Date columns support the full date shorthand system — the user can type t (today), t+5 (5 days from now), 1203 (12th March), or 12-03-2026.
See HeaderLines for the full column reference.
Calculated columns (formula)
A column can have a formula — an arithmetic expression that the client evaluates live after every cell edit. No server call needed.
json
{
"id": "line_amount",
"label": "Amount",
"type": "Decimal",
"editable": false,
"formula": "quantity * unit_price * (1 - line_discount_pct / 100)",
"validation": { "decimals": 2 }
}When the user changes quantity, unit price, or discount on any row, the Amount column updates instantly.
How it works
- After any cell edit is confirmed (Enter, Tab), the client scans all columns in the current row
- For each column with a
formula, it reads the values of the referenced columns from the same row - It evaluates the arithmetic expression and writes the result to the cell
- Totals footer with
source_columnalso recalculates
Formula syntax
Formulas reference other column IDs by name:
| Element | Example |
|---|---|
| Column ID | quantity, unit_price |
| Number | 100, 0.5 |
| Operators | +, -, *, / |
| Parentheses | (1 - discount / 100) |
Examples:
quantity * unit_price // simple total
quantity * unit_price * (1 - discount / 100) // with discount
amount * tax_rate / 100 // tax calculation
price - cost // marginEdge cases
- Empty or non-numeric values — treated as 0
- Division by zero — result is 0
- Missing column — treated as 0
- Precision — uses
validation.decimalsif set, otherwise defaults to 2 decimal places
Formula vs server calculation
The formula provides instant feedback while the user is editing. The server should still recalculate on save (as the source of truth). Both produce the same result — the formula is a client-side preview, not a replacement for server validation.
Formula columns should be editable: false — the user cannot type in them.
Totals footer
The totals array defines label+value pairs rendered in a footer row between the grid and the bottom bar. Values are right-aligned.
json
"totals": [
{"label": "Starting Balance:", "value": "1,200.00"},
{"label": "Balance:", "value": "0.00", "source_column": "amount"}
]TotalField properties
| Property | Type | Required | Purpose |
|---|---|---|---|
label | string | yes | Display label (e.g., "Balance:") |
value | string | yes | Formatted value (e.g., "1,200.00") |
source_column | string | no | Column id to aggregate. When set, the client computes live |
aggregate | string | no | "sum" (default), "count", "avg", "min", "max" |
decimals | integer | no | Formatting precision (default 2) |
If totals is empty or absent, no footer row is shown and the grid uses the full available height.
The totals field is rendered on Grid screens only. On other layout types the field is accepted but not displayed.
Live totals
When a total has source_column, the client computes its value in real-time from the grid data — no save roundtrip needed. The value field is still required (the server provides the initial value), but the client overwrites it as the user edits, inserts, or deletes rows.
Totals without source_column remain server-computed and update only when the server sends a new response (e.g., after save).
Both types can coexist in the same totals array — see the Banking Journal example above where "Starting Balance:" is server-computed and "Balance:" is live.
Supported aggregates:
| Aggregate | Description |
|---|---|
sum | Sum of all values (default) |
count | Number of non-empty values |
avg | Average of all values |
min | Minimum value |
max | Maximum value |
How saves work with Grid
When the user presses Ctrl+S, the client sends a SaveChangeset with the full grid data. Since there are no header fields, changes is empty:
json
{
"screen_id": "banking_journal",
"record_id": "",
"changes": {},
"lines": [
[
"16-03-2026",
"Payment",
"PAY-001",
"Customer",
"10000",
"Acme Corp",
"-500.00"
],
[
"16-03-2026",
"Invoice",
"INV-100",
"Customer",
"20000",
"Selangorian Ltd.",
"1200.00"
]
]
}Your server parses lines by column position (same as HeaderLines), persists them, and returns a fresh ScreenContract with updated totals.
See Saving for the full save protocol.
Posting
Posting is handled through Screen Actions. Define a confirm kind action in screen_actions — the user triggers it via Ctrl+A and the server endpoint processes the journal lines.
See the Banking Journal example for the full posting flow.
Grid vs HeaderLines
| Grid | HeaderLines | |
|---|---|---|
| Layout | Full-screen grid | Card + grid overlay |
| Card fields | None | Header sections with fields |
| Opening | Grid visible immediately | User presses Ctrl+L |
| Escape | Goes back to previous screen | Closes the grid overlay |
| Totals footer | Shown if totals is set | Not shown |
| Use for | Batch entry, payment journals | Documents (Sales Order) |