Skip to content

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 header
  • sections is empty — journals have no header fields
  • totals — "Starting Balance:" is server-computed (frozen until save); "Balance:" has source_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_type and account_type are Options — the user cycles through choices with Space
  • account_no has a lookup — the user can look up customers
  • screen_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=Customer

Your 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:

  1. Writes "10000" to the account_no cell (from value_column)
  2. Writes "The Cannon Group" to the description cell (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=Customer

Your 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

  1. Parse lines — each array maps to columns by position
  2. Skip empty rows — trailing empty rows are ignored
  3. Persist — save journal lines to storage
  4. Compute totals — sum the amounts
  5. 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/post
json
{
  "action_id": "post_journal",
  "screen_title": "Banking Journal",
  "record_id": "",
  "fields": {}
}

What your server does

  1. Load the journal lines from storage
  2. For each line — create a ledger entry with a sequential entry number
  3. Update customer balances — add the amount to each customer's balance
  4. Clear the journal — delete all journal lines
  5. 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

EndpointMethodPurpose
/screen/journalGETLoad the journal (Grid ScreenContract)
/screen/journal/savePOSTSave journal lines
/lookup/account_no?type=CustomerGETCustomer lookup list
/validate/account_no/{value}?type=CustomerGETValidate customer + autofill description
/action/journal/postPOSTPost journal lines (screen action)
/action/journal/exportPOSTExport journal to CSV (screen action)