Screen Actions
Summary
Screen actions are server-driven contextual operations that go beyond save and delete. Send an email, print an invoice, change a status, post a journal — your server declares what actions are available, and the client renders a picker, collects input if needed, and posts back to your endpoint.
Actions are different from the actions map (which handles save/delete/create URLs). Screen actions are custom operations defined per screen in the screen_actions array.
How it works
- Your server includes
screen_actionsin the ScreenContract - The client shows
Ctrl+A Actionsin the bottom bar when actions exist - The user presses Ctrl+A (or F8) to open the action picker
- The user selects an action — the client handles confirmation or form input based on the action's
kind - The client POSTs an ActionRequest to the action's
endpoint - Your server processes it and returns an ActionResponse
- The client shows the result in a modal the user must acknowledge
ActionDef
Each action in the screen_actions array is an ActionDef:
json
{
"id": "send_email",
"label": "Send as Email",
"kind": "modal",
"fields": [],
"endpoint": "/action/sales_order/SO-1001/send_email",
"confirm_message": null
}| Property | Type | Required | Default | Purpose |
|---|---|---|---|---|
id | string | Yes | — | Unique identifier for this action |
label | string | Yes | — | Display text in the action picker |
kind | string | No | "simple" | How the client handles this action |
fields | array | No | [] | Form fields (only used when kind is "modal") |
endpoint | string | Yes | — | URL the client POSTs to when executing |
confirm_message | string | No | null | Confirmation prompt (only for "confirm" kind) |
icon | string | No | null | Reserved for future use |
Action kinds
Simple
Fire-and-forget. The user selects the action, and the client immediately POSTs to the endpoint.
json
{
"id": "export_csv",
"label": "Export to CSV",
"kind": "simple",
"endpoint": "/action/journal/export"
}Confirm
Shows a confirmation dialog with Yes/No before executing. Use this for destructive or irreversible operations.
json
{
"id": "post_journal",
"label": "Post Journal",
"kind": "confirm",
"endpoint": "/action/journal/post",
"confirm_message": "Post all journal lines? This cannot be undone."
}If confirm_message is omitted, the client shows "Execute this action?".
Modal
Opens a form with input fields. The user fills in the fields and submits. The field values are included in the ActionRequest.
json
{
"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",
"required": true
},
{
"id": "subject",
"label": "Subject",
"type": "Text",
"value": "Sales Order SO-1001"
},
{
"id": "message",
"label": "Message",
"type": "Text",
"value": ""
}
]
}The form uses the same field rendering as card fields — full editing with cursor, selection, and field navigation. Pre-fill the value property with defaults from your data (e.g., the customer's email address).
ActionField
Fields in a modal action form:
| Property | Type | Required | Default | Purpose |
|---|---|---|---|---|
id | string | Yes | — | Field identifier (sent in ActionRequest) |
label | string | Yes | — | Display label |
type | string | Yes | — | Field type (same as card field types) |
value | string | No | "" | Pre-filled value |
required | bool | No | false | Whether the field must have a value |
options | array | No | null | Options for Option type fields |
placeholder | string | No | null | Placeholder text |
validation | object | No | null | Validation rules (same as card field validation) |
The type property accepts the same values as card fields: Text, Email, Decimal, Integer, Date, Phone, URL, Boolean, Option, Password, TextArea, Time.
Option fields cycle through values with Space, just like on cards.
ActionRequest
When the user executes an action, the client POSTs:
json
{
"action_id": "send_email",
"screen_title": "Sales Order - SO-1001",
"record_id": "SO-1001",
"fields": {
"to": "ap@cannongroup.example",
"subject": "Sales Order SO-1001",
"message": "Please review the attached order."
}
}| Property | Type | Purpose |
|---|---|---|
action_id | string | The id from the ActionDef |
screen_title | string | The human-readable screen title (e.g. "Sales Order - SO-1001") |
record_id | string | From ScreenContract.record_id, or null if empty |
fields | object | Field values from the form (empty for Simple and Confirm) |
ActionResponse
Your server returns:
json
{
"success": true,
"message": "Email sent to ap@cannongroup.example.",
"screen": null,
"error": null
}| Property | Type | Purpose |
|---|---|---|
success | bool | Whether the action completed successfully |
message | string | Success message shown to the user |
error | string | Error message shown to the user (on failure) |
redirect_url | string | Clears navigation history and navigates to this URL after dismissal |
push_url | string | Pushes this URL onto the navigation stack after dismissal |
screen | ScreenContract | Replaces the current screen inline (lower priority than URL fields) |
The client shows the message or error in a result modal that the user must dismiss with Enter.
Only set one navigation field per response. If multiple are set, the client applies the highest-priority one: redirect_url → push_url → screen. On failure (success: false), all navigation fields are ignored.
Success
Return success: true with a message:
json
{
"success": true,
"message": "Email sent to ap@cannongroup.example."
}Success with screen update
If the action changes data, return an updated ScreenContract to refresh the display:
json
{
"success": true,
"message": "Journal posted. 3 entries created.",
"screen": {
"layout": "Grid",
"title": "Banking Journal",
"lines": { "columns": [], "rows": [] }
}
}Success with redirect
Use redirect_url when an action transforms the current record into something else — posting an invoice, converting a quote to an order. The original record no longer exists, so the client clears navigation history and opens the new screen. Escape from there follows the destination screen's parent_url (see ScreenContract parent_url).
json
{
"success": true,
"message": "Invoice posted.",
"redirect_url": "/screen/posted_invoice/42"
}If redirect_url is an external URL (starts with http:// or https:// pointing to a different host), the client opens it in the system default browser instead of navigating to it.
Success with push
Use push_url when an action opens a related screen without destroying the current one — a print preview, a report, an analytics view. The destination is pushed onto the navigation stack. Escape returns to the screen the action was triggered from.
json
{
"success": true,
"message": "Report ready.",
"push_url": "/screen/invoice_report/42"
}Navigation after a successful action — summary
Applied in priority order. Only set one per response.
| Priority | Response field | Client behavior | Escape from destination |
|---|---|---|---|
| 1 | redirect_url | Clear history, navigate to URL | Follows destination's parent_url |
| 2 | push_url | Push URL onto stack | Returns to current screen |
| 3 | screen | Replace current screen inline | Unchanged |
| — | (none) | Refresh current URL (404 → pop back to parent) | Unchanged |
Without any navigation field, the client refreshes the current URL after a successful action. If the record returns 404 (deleted or transformed), the client automatically pops back to the previous list.
Error
Return success: false with an error:
json
{
"success": false,
"error": "A valid email address is required."
}The client shows the error in the result modal. The user can retry the action.
Adding actions to a screen
Add screen_actions to your ScreenContract response:
json
{
"layout": "HeaderLines",
"title": "Sales Order - SO-1001",
"sections": [],
"lines": {},
"actions": {
"save": "/screen/sales_order/SO-1001/save",
"delete": "/screen/sales_order/SO-1001/delete"
},
"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 Ctrl+A Actions hint appears in the bottom bar only when screen_actions is non-empty.
Actions work on all screen types: Card, List, HeaderLines, and Grid. On HeaderLines screens, Ctrl+A works from both the card header and the lines overlay.
Action endpoint routes
Action endpoints follow the same URL architecture as other routes. The client resolves the endpoint path by prepending the scheme and host.
| Pattern | Method | Request body | Response |
|---|---|---|---|
/action/{resource}/{id}/{action_id} | POST | ActionRequest | ActionResponse |
/action/{resource}/{action_id} | POST | ActionRequest | ActionResponse |
These patterns are conventions. Use any URL structure you want — the client posts to whatever endpoint you provide in the ActionDef.
For complete worked examples, see Sales Order (modal email action) and Banking Journal (confirm posting action).