Example: Banking Journal
Summary
A complete worked example of a Banking Journal — a full-screen editable grid for registering payments against customer accounts. This walks through the full JSON for the Grid screen, account lookups with autofill, saving, and posting to the ledger.
By the end, you will understand how the Grid layout, editable grid columns, lookups, totals footer, and screen actions work together.
What the user sees
The Banking Journal is a single full-screen grid. The user enters journal lines — each line records a payment, invoice, or credit memo against a customer account.
┌─ Banking Journal ────────────────────────────────────────────────────┐
│ Posting Date Document Type Document No. Acc. Type Account No. … Amt│
│ ─────────── ────────────── ───────────── ───────── ─────────── ──────│
│ 16-03-2026 Payment PAY-001 Customer 10000 -500 │
│ 16-03-2026 Invoice INV-100 Customer 20000 1200 │
│ │
│ │
│ Starting Balance: 700.00 Balance: 700.00 │
├──────────────────────────────────────────────────────────────────────┤
│ Balance: 700.00 Ctrl+S Save F3 Insert Ctrl+A Actions Esc Close │
└──────────────────────────────────────────────────────────────────────┘Step 1: The ScreenContract
Your server returns a Grid layout with grid columns and a totals footer:
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": "document_no",
"label": "Document No.",
"type": "Text",
"width": 14,
"editable": true
},
{
"id": "account_type",
"label": "Acc. Type",
"type": "Option",
"width": 10,
"editable": true,
"options": [
"",
"Customer"
]
},
{
"id": "account_no",
"label": "Account No.",
"type": "Text",
"width": 12,
"editable": true,
"lookup": {
"endpoint": "/lookup/account_no",
"validate": "/validate/account_no",
"context": [{ "field": "account_type", "param": "type" }]
}
},
{
"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": [
{
"index": 0,
"values": [
"16-03-2026",
"Payment",
"PAY-001",
"Customer",
"10000",
"The Cannon Group",
"-500.00"
]
},
{
"index": 1,
"values": [
"16-03-2026",
"Invoice",
"INV-100",
"Customer",
"20000",
"Selangorian Ltd.",
"1200.00"
]
},
{
"index": 2,
"values": [
"",
"",
"",
"",
"",
"",
""
]
}
],
"row_count": 3,
"editable": true,
"selectable": true
},
"totals": [
{
"label": "Starting Balance:",
"value": "700.00"
},
{
"label": "Balance:",
"value": "700.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."
},
{
"id": "export_csv",
"label": "Export to CSV",
"kind": "simple",
"endpoint": "/action/journal/export"
}
]
}Key things to notice
layout: "Grid"— full-screen grid, no card headersectionsis empty — journals have no header fieldstotals— "Starting Balance:" is server-computed (frozen until save); "Balance:" hassource_column: "amount"so it updates live as the user edits- All columns are
editable: true— every cell is for data entry - Row 2 is empty — the "new line" row where the user starts entering data
document_typeandaccount_typeare Options — the user cycles through choices with Spaceaccount_nohas a lookup — the user can look up customersscreen_actions— "Post Journal" (confirm kind) and "Export to CSV" (simple kind) are available via Ctrl+A
Step 2: The user adds a new line
The user navigates to the empty row (row 2) and enters a payment.
2a. Enter the date
The user types t and presses Tab. The client's date parser expands t to today's date (e.g., 16-03-2026). All date shorthands work: t+5, t-3, 12, 1203, 12-03-26.
No server call. Date parsing happens entirely in the client.
2b. Select the Document Type
The user presses Space to cycle to "Payment" (or opens the modal with Ctrl+Enter to pick from the list).
No server call. Option cycling is local.
2c. Enter a Document No.
The user types PAY-002 and presses Tab.
No server call. Free text entry.
2d. Select the Account Type
The user presses Space to set "Customer".
No server call.
2e. Look up an Account No.
The user presses Ctrl+Enter on the Account No. cell. The client reads the account_type column value ("Customer") and sends:
GET /lookup/account_no?type=CustomerYour server returns the customer list:
json
{
"layout": "List",
"title": "Customers",
"lines": {
"columns": [
{
"id": "no",
"label": "No.",
"width": 10
},
{
"id": "name",
"label": "Name",
"width": "fill"
},
{
"id": "city",
"label": "City",
"width": 15
},
{
"id": "phone",
"label": "Phone",
"width": 16
}
],
"rows": [
{
"index": 0,
"values": [
"10000",
"The Cannon Group",
"Tórshavn",
"+45 70102030"
]
},
{
"index": 1,
"values": [
"20000",
"Selangorian Ltd.",
"London",
"+44 20 1234 5678"
]
}
],
"selectable": true,
"value_column": "no",
"autofill": {
"name": "description"
}
}
}The autofill map says: when the user selects a customer, copy the name column into the description cell.
2f. User selects "10000 - The Cannon Group"
The client:
- Writes
"10000"to theaccount_nocell (fromvalue_column) - Writes
"The Cannon Group"to thedescriptioncell (from autofill)
2g. Alternative: type the account number directly
The user types 10000 and presses Tab. The client sends:
GET /validate/account_no/10000?type=CustomerYour server returns:
json
{
"valid": true,
"autofill": {
"description": "The Cannon Group"
}
}Same result — the description fills in automatically. If the customer number is invalid:
json
{
"valid": false,
"error": "'99999' is not a valid account number."
}The client locks the user on the cell until they fix the value or press Esc to revert.
2h. Enter the amount
The user types -500 and presses Tab. The client normalizes it to -500.00.
Step 3: Saving
The user presses Ctrl+S. The client sends the full grid data:
json
{
"screen_id": "banking_journal",
"record_id": "",
"changes": {},
"lines": [
[
"16-03-2026",
"Payment",
"PAY-001",
"Customer",
"10000",
"The Cannon Group",
"-500.00"
],
[
"16-03-2026",
"Invoice",
"INV-100",
"Customer",
"20000",
"Selangorian Ltd.",
"1200.00"
],
[
"16-03-2026",
"Payment",
"PAY-002",
"Customer",
"10000",
"The Cannon Group",
"-500.00"
]
]
}What your server does
- Parse lines — each array maps to columns by position
- Skip empty rows — trailing empty rows are ignored
- Persist — save journal lines to storage
- Compute totals — sum the amounts
- Return a fresh ScreenContract — with updated totals
The save response
json
{
"layout": "Grid",
"title": "Banking Journal",
"status": "Saved.",
"sections": [],
"lines": {
"columns": [],
"rows": [
{
"index": 0,
"values": [
"16-03-2026",
"Payment",
"PAY-001",
"Customer",
"10000",
"The Cannon Group",
"-500.00"
]
},
{
"index": 1,
"values": [
"16-03-2026",
"Invoice",
"INV-100",
"Customer",
"20000",
"Selangorian Ltd.",
"1200.00"
]
},
{
"index": 2,
"values": [
"16-03-2026",
"Payment",
"PAY-002",
"Customer",
"10000",
"The Cannon Group",
"-500.00"
]
},
{
"index": 3,
"values": [
"",
"",
"",
"",
"",
"",
""
]
}
],
"editable": true
},
"totals": [
{
"label": "Balance:",
"value": "200.00"
},
{
"label": "Total Balance:",
"value": "200.00"
}
],
"actions": {
"save": "/screen/journal/save"
}
}The client replaces the current screen. The totals footer now shows 200.00.
Step 4: Posting
Posting is a screen action, not a hardcoded shortcut. The user presses Ctrl+A (or F8) and selects "Post Journal". The client shows the confirmation dialog:
Post all journal lines? This cannot be undone.
The user confirms. The client POSTs to the action endpoint:
POST /action/journal/postjson
{
"action_id": "post_journal",
"screen_title": "Banking Journal",
"record_id": "",
"fields": {}
}What your server does
- Load the journal lines from storage
- For each line — create a ledger entry with a sequential entry number
- Update customer balances — add the amount to each customer's balance
- Clear the journal — delete all journal lines
- Return an ActionResponse with the updated screen
The post response
json
{
"success": true,
"message": "Posted 3 entries.",
"screen": {
"layout": "Grid",
"title": "Banking Journal",
"sections": [],
"lines": {
"columns": [],
"rows": [],
"editable": true
},
"totals": [
{
"label": "Balance:",
"value": "0.00"
},
{
"label": "Total Balance:",
"value": "0.00"
}
],
"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."
},
{
"id": "export_csv",
"label": "Export to CSV",
"kind": "simple",
"endpoint": "/action/journal/export"
}
]
}
}The client shows "Posted 3 entries." in a result modal. The user presses Enter to dismiss. The grid is now empty — the journal lines have been converted into permanent ledger entries. The user can navigate to a customer card to see the updated balance and new ledger entries.
Summary of all server endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/screen/journal | GET | Load the journal (Grid ScreenContract) |
/screen/journal/save | POST | Save journal lines |
/lookup/account_no?type=Customer | GET | Customer lookup list |
/validate/account_no/{value}?type=Customer | GET | Validate customer + autofill description |
/action/journal/post | POST | Post journal lines (screen action) |
/action/journal/export | POST | Export journal to CSV (screen action) |