Skip to content

ScreenContract

Summary

Every JSON response from your server (except ValidateResponse and AuthResponse) is a ScreenContract. This is the universal page format.

See protocol.md for the backwards compatibility policy.

The structure

json
{
  "layout": "Card",
  "title": "Customer Card - 10000",
  "screen_id": "customer_card",
  "sections": [],
  "lines": null,
  "menu": null,
  "status": null,
  "work_date": "10-03-2026",
  "locale": {
    "date_format": "DD-MM-YYYY",
    "decimal_separator": ",",
    "thousand_separator": "."
  },
  "ui_strings": null,
  "auth_action": null,
  "user_display_name": "Erik Y.",
  "actions": {
    "save": "/screen/customer_card/10000/save",
    "delete": "/screen/customer_card/10000/delete",
    "create": "/screen/customer_card/new"
  },
  "lines_overlay_pct": 50,
  "screen_actions": []
}

All fields

FieldTypeRequiredDefaultPurpose
layoutstringYesHow the client renders this screen: "Card", "List", "HeaderLines", "Grid", "Menu"
titlestringYesScreen title shown in the title bar
screen_idstringYesMachine identifier for this screen (e.g. "customer_card"). Echoed back in SaveChangeset and DeleteRequest. Must be stable snake_case. Use "" for screens with no record identity (login, menus).
sectionsarrayNo[]Field groups for Card and HeaderLines layouts
linesobjectNonullTable data for List, HeaderLines, Grid, and lookup screens
menuobjectNonullMenu structure for Menu layout
statusstringNonullMessage shown in the bottom status bar
work_datestringNonullWork date for the session (used by date field shortcuts). Format follows locale.date_format.
localeobjectNonullDate/number formatting settings
ui_stringsobjectNonullServer-driven UI text for localization
auth_actionstringNonullWhen set, the form submits as an AuthRequest to this URL
user_display_namestringNonullLogged-in user's name (shown in the top-right corner on all screens)
actionsobjectNo{}Action URLs keyed by name. Values are always URLs.
record_idstringNo""The current record's machine identifier (e.g. "10000"). Included in ActionRequest when the user executes a screen action. Leave empty for screens with no record identity (menus, journals).
lines_overlay_pctintegerNo50Lines overlay height percentage (0–100). Only used on HeaderLines screens.
lines_openboolNofalseWhen true, the lines overlay opens automatically on HeaderLines screens.
totalsarrayNo[]Summary label+value pairs shown in a footer row below the grid
screen_actionsarrayNo[]Contextual actions (send email, print, post, etc.). See Screen Actions
parent_urlstringNonullThe natural parent of this screen. See Navigation and parent_url.

Layout types

LayoutUse forKey content
CardSingle-record form (Customer Card, Item Card)sections with fields
ListTable view (Customer List, Lookup List)lines with columns and rows
HeaderLinesDocument with header + line items (Sales Order)sections + lines + lines_overlay_pct
GridFull-screen editable grid (journals, batch entry)lines with columns and rows
MenuNavigation menumenu with tabs and items

The actions map

Provide URLs for save, delete, and create operations:

json
{
  "actions": {
    "save": "/screen/customer_card/10000/save",
    "delete": "/screen/customer_card/10000/delete",
    "create": "/screen/customer_card/new"
  }
}

The client uses these URLs when the user presses Ctrl+S (save), Ctrl+D (delete), or Ctrl+N (new). Your server controls all routing — the client never constructs action URLs.

Include only the actions that are valid for this screen. If a record cannot be deleted, omit delete. All values in actions are URLs.

The screen_actions array

For custom operations beyond save/delete (send email, print invoice, post journal, change status), use screen_actions:

json
{
  "screen_actions": [
    {
      "id": "send_email",
      "label": "Send as Email",
      "kind": "modal",
      "endpoint": "/action/sales_order/SO-1001/send_email",
      "fields": [
        {
          "id": "to",
          "label": "To",
          "type": "Email",
          "value": "ap@cannongroup.example"
        }
      ]
    }
  ]
}

The client shows Ctrl+A Actions in the bottom bar when this array is non-empty. See Screen Actions for the full reference.

The status field

Use status to show messages after operations:

json
{
  "status": "Saved."
}

For save errors that are business logic (not infrastructure), return HTTP 200 with the error in status:

json
{
  "layout": "Card",
  "title": "Customer Card - 10000",
  "status": "Cannot save: Name is required.",
  "sections": []
}

The client shows the status text in the bottom bar. This keeps the card open so the user can see what went wrong.

Minimal examples

Minimal Card

json
{
  "layout": "Card",
  "title": "Customer Card",
  "screen_id": "customer_card",
  "sections": [
    {
      "id": "general",
      "label": "General",
      "fields": [
        {
          "id": "name",
          "label": "Name",
          "type": "Text",
          "value": "Acme Corp"
        }
      ]
    }
  ],
  "actions": {
    "save": "/screen/customer_card/1/save"
  }
}

Minimal List

json
{
  "layout": "List",
  "title": "Customers",
  "screen_id": "customer_list",
  "lines": {
    "columns": [
      {
        "id": "no",
        "label": "No.",
        "width": 10
      },
      {
        "id": "name",
        "label": "Name",
        "width": "fill"
      }
    ],
    "rows": [
      {
        "index": 0,
        "values": [
          "10000",
          "Acme Corp"
        ]
      }
    ],
    "selectable": true,
    "on_select": "/screen/customer_card/{0}"
  }
}

Minimal Grid

json
{
  "layout": "Grid",
  "title": "Payment Journal",
  "screen_id": "payment_journal",
  "sections": [],
  "lines": {
    "columns": [
      {
        "id": "date",
        "label": "Date",
        "type": "Date",
        "width": 12,
        "editable": true
      },
      {
        "id": "description",
        "label": "Description",
        "type": "Text",
        "width": "fill",
        "editable": true
      },
      {
        "id": "amount",
        "label": "Amount",
        "type": "Decimal",
        "width": 14,
        "align": "right",
        "editable": true
      }
    ],
    "rows": [],
    "editable": true
  },
  "totals": [
    {
      "label": "Balance:",
      "value": "0.00"
    }
  ],
  "actions": {
    "save": "/screen/journal/save"
  }
}

Minimal Menu

json
{
  "layout": "Menu",
  "title": "Main Menu",
  "screen_id": "",
  "menu": {
    "panel_title": "Main Menu",
    "tabs": [
      {
        "label": "Sales",
        "items": [
          {
            "label": "Customers",
            "action": {
              "type": "open_screen",
              "url": "/screen/customer_list"
            }
          }
        ]
      }
    ]
  }
}

parent_url is the screen the user would naturally expect to go back to from this screen.

The client uses it as a fallback when there is no navigation history — the most common case being after an action redirect_url lands the user on a new screen. During normal user navigation the stack is always populated and parent_url is ignored.

json
{
  "layout": "Card",
  "title": "Posted Sales Invoice - PSI-01018",
  "parent_url": "/screen/posted_invoice_list",
  ...
}

When to set it:

Screen typeTypical parent_url
CardThe list for this resource
HeaderLinesThe list for this resource
Listnull (top-level) or a sub-menu URL
Gridnull or menu URL
Menu / Loginnull — always

How it chains: If the client follows parent_url to a list screen, and that list screen also declares a parent_url, Escape from the list follows that too. The full back-chain is built from the server's declarations, not from client history.

In the Laravel plugin, parent_url is set automatically — Card and HeaderLines screens default to the resource's list URL, lists default to null. Override parentUrl() on resources where the default does not fit.