Skip to content

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 → ScreenContract

Response 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.
  • sections is 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 via Ctrl+A. See Screen Actions.
  • No create or delete action — journal lines are managed within the grid (Ctrl+N inserts, Ctrl+D deletes 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

  1. After any cell edit is confirmed (Enter, Tab), the client scans all columns in the current row
  2. For each column with a formula, it reads the values of the referenced columns from the same row
  3. It evaluates the arithmetic expression and writes the result to the cell
  4. Totals footer with source_column also recalculates

Formula syntax

Formulas reference other column IDs by name:

ElementExample
Column IDquantity, unit_price
Number100, 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                                     // margin

Edge cases

  • Empty or non-numeric values — treated as 0
  • Division by zero — result is 0
  • Missing column — treated as 0
  • Precision — uses validation.decimals if 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.

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

PropertyTypeRequiredPurpose
labelstringyesDisplay label (e.g., "Balance:")
valuestringyesFormatted value (e.g., "1,200.00")
source_columnstringnoColumn id to aggregate. When set, the client computes live
aggregatestringno"sum" (default), "count", "avg", "min", "max"
decimalsintegernoFormatting 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:

AggregateDescription
sumSum of all values (default)
countNumber of non-empty values
avgAverage of all values
minMinimum value
maxMaximum 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

GridHeaderLines
LayoutFull-screen gridCard + grid overlay
Card fieldsNoneHeader sections with fields
OpeningGrid visible immediatelyUser presses Ctrl+L
EscapeGoes back to previous screenCloses the grid overlay
Totals footerShown if totals is setNot shown
Use forBatch entry, payment journalsDocuments (Sales Order)