Actions and lifecycle
A screen with a grid + a dialog covers the static CRUD flow. A screen with actions and lifecycle hooks extends that to anything else — run a custom query on a button click, navigate to another screen on a row click, fire a webhook after a save, refresh after a delete.
The Actions tab of the Screen Designer is where all of this is wired. Three groups, the same action shapes everywhere.
The three action groups
The Actions tab organises every action surface into three groups:
| Group | Hooks | When they fire | ParamBind resolves against |
|---|---|---|---|
| Dialog hooks | on_load, on_save, on_cancel | While the form is open. | The live form state. |
| Toolbar | actions (screen-level) | The user clicks a toolbar button. | The currently-selected row (empty if none). |
| Row hooks | on_insert, on_update, on_delete | After a mutation succeeds — works whether from the dialog Save or the inline grid Save. | The new row (on_insert / on_update) or the deleted row (on_delete). |
A separate tab — Row menu — carries row_menu: actions shown when the user right-clicks a row. ParamBinds resolve against the clicked row.
The seven core action types
Every action has a type field discriminating one of seven core variants (plus four composite variants below).
| Type | What it does | Key fields |
|---|---|---|
run_query | Execute a connector query (writable or read). | connector, query, params (ParamBinds). |
call_api | Call an HTTP / API connector endpoint. | connector, endpoint, params. |
navigate | Open another screen as a modal, narrowed by ParamBinds. | connector, screen, params. |
set_field | Change a dialog field's value. | field, value (literal or source from another field). |
confirm | Prompt the user for confirmation; block the chain on Cancel. | title, message, confirm_label. |
notify | Show a toast / banner. | level (info / warning / error / success), message. |
refresh | Re-run the screen's read query (reload the grid). | (no fields). |
The action editor's Type dropdown lists all seven; switching type reveals the fields appropriate to it. Target dropdowns are populated from the live connectors / screens registry — switching the destination refreshes them.
The four composite types
Built on top of the seven cores — used to express more complex flows in one action entry instead of stitching many.
| Type | What it does |
|---|---|
chain | A sequence of nested actions, each step's output available to the next via <step_id>.first_row.<col> references. Stops on the first error unless an action sets stop_on_error = false. |
if | Conditional branching inside a chain — runs then actions when the condition holds, else actions otherwise. |
loop | Iterate over an array (typically the result of a previous run_query) and run the body for each item. |
return | Bind chain-context values back into dialog fields. Used at the end of a chain to surface computed values on the form. |
Most screens never need composites — a flat list of run_query + refresh + notify covers 80% of cases. Reach for chain when the same trigger needs to fire several queries that share intermediate values; for if / loop when the flow truly branches or iterates.
ParamBind context per fire
Every action carries params (ParamBinds). The bind resolves the right way depending on where the action fires:
| Where | source reads from |
|---|---|
Toolbar (actions) | The currently-selected row's columns. If no row is selected, sources resolve to NULL (unless a default is set). |
Row menu (row_menu) | The clicked row's columns. |
Dialog on_load | The just-loaded form state. |
Dialog on_save | The form state at submit time (after the main write succeeded). |
Dialog on_cancel | The form state when the user pressed Cancel. |
Row on_insert / on_update | The new row's values. |
Row on_delete | The deleted row's values. |
Inside a chain | The chain context — <previous_step_id>.first_row.<col> reads the named step's output. |
The two reserved binds — #LOGIN_USER# and #SYSDATE# — work everywhere as source values. See Parameter binding for the full reference.
Common patterns
Refresh the grid after a write
The most common toolbar action: run a stored procedure, then reload the grid so the user sees the result.
[[screens.crm.customers.actions]]
type = "run_query"
id = "compute_balances"
connector = "crm"
query = "compute_balances_post"
label = "Recompute balances"
[[screens.crm.customers.actions]]
type = "refresh"
id = "reload_grid"
The two actions are separate entries — they fire in order. If the first fails (write rejected, network error), the second doesn't fire and the grid is unchanged.
Confirm before destructive
A Delete row action on the row menu, gated by a confirmation:
[[screens.crm.customers.row_menu]]
type = "confirm"
id = "confirm_delete"
title = "Delete customer?"
message = "This removes the customer and every related deal."
[[screens.crm.customers.row_menu]]
type = "run_query"
id = "do_delete"
connector = "crm"
query = "customers_delete"
[[screens.crm.customers.row_menu.params]]
param = "CUSTOMER_ID"
source = "CUSTOMER_ID"
If the user clicks Cancel on the confirm, the chain stops; the delete doesn't fire.
Stamp audit columns on insert
The screen has a column CREATED_BY you want filled with the current user automatically — but the dialog doesn't expose it (it's a hidden audit column). Use on_insert:
[[screens.crm.customers.on_insert]]
type = "run_query"
id = "stamp_audit"
connector = "crm"
query = "customers_stamp_audit"
[[screens.crm.customers.on_insert.params]]
param = "CUSTOMER_ID"
source = "CUSTOMER_ID"
[[screens.crm.customers.on_insert.params]]
param = "CREATED_BY"
source = "#LOGIN_USER#"
The main INSERT fires first; if it succeeds, this audit-stamp UPDATE fires with the same row's id + the session user. The screen never surfaces CREATED_BY in the dialog, but the column ends up populated.
For audit logging that should mirror every write, prefer audit_table on the General tab — it adds AUD_ACTION / AUD_USER / AUD_DATE rows automatically without explicit hooks.
Notify on success
Wire a notify after a long-running action to give the user feedback:
[[screens.crm.customers.actions]]
type = "run_query"
id = "export_pdf"
connector = "crm"
query = "customers_export_pdf"
label = "Export to PDF"
[[screens.crm.customers.actions]]
type = "notify"
id = "notify_done"
level = "success"
message = "Export complete — check your downloads."
level accepts info / warning / error / success; each renders with a different colour.
Open a sibling screen with bound filters
navigate opens another screen as a modal, narrowed by the binds. Useful from a row menu:
[[screens.crm.customers.row_menu]]
type = "navigate"
id = "view_deals"
connector = "crm"
screen = "deals"
label = "View deals for this customer"
[[screens.crm.customers.row_menu.params]]
param = "CUSTOMER_ID"
source = "CUSTOMER_ID"
The clicked row's CUSTOMER_ID binds into the deals screen's read query; the deals modal opens showing only that customer's deals.
For row-click (left-click, not right-click), use the General tab's row-click behaviour instead (row_click_screen + row_click_binds) — it's the same shape, just triggered on click.
The action editor
Each action group has an + Add action button. Clicking adds a row prompted for an action id (must be unique within the list). The editor reveals the type-specific fields:
| Field | Notes |
|---|---|
| id | Unique within the action list. Used as the chain-context key (<id>.first_row.<col>). |
| type | Dropdown — switches the available fields below. |
| Target | The query / endpoint / screen the action operates on (label depends on type: Query for run_query, Endpoint for call_api, Target query for navigate). |
| Params | List of ParamBinds. Click + Add binding to add one. |
| stop_on_error | When false, the chain continues even if this action fails. Default true. |
| prompt | Optional PromptField list — surfaces an input form before the action fires (e.g. ask the user for an END_DATE before running a report). |
The Wrap in chain button on any action wraps it inside a chain — useful when you started flat and now need conditional logic or a loop.
Save flow
The Actions tab edits the screen's actions / row_menu / on_* lists; the Screen Designer's Save commits all of them in one shot to screens.toml. Hot reload picks them up immediately — the next click on the toolbar button fires the updated action chain.
Common pitfalls
| Mistake | Symptom | Fix |
|---|---|---|
on_save chain that fires before the main write. | The chain hits a race condition — the row doesn't exist yet. | on_save fires after the main update/insert succeeds. If the chain needs the row's new id, source it from the form state (or use chain with the right ordering). |
Toolbar action with source = COLUMN but no row selected. | The bind resolves to NULL; the query fails or operates on the wrong row. | Either set a default on the bind, or move the action to the row menu so it always fires with a row in context. |
refresh without a preceding write. | The grid reloads but nothing changed — confusing UX. | Use refresh after writes, or when an external action (a webhook ack) changed the data the grid shows. |
confirm after the destructive action instead of before. | The user gets a "are you sure?" prompt after the delete already fired. | confirm must be the first action in the chain. |
Stamping CREATED_BY in on_save but the column is required in the dialog. | The dialog Save fails because the column is empty. | Either don't make the column required in the dialog (the hook fills it), or use the dictionary's LOGIN rule on the column to pre-fill at form-load. |
What's next
- Nested tabs — embed a child-record form or a related-rows grid.
- Parameter binding — the full ParamBind reference.
- Concepts → Screens — deep reference for action types and lifecycle.