Skip to main content

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:

Screen Designer · crm.customers · ActionsDIALOG HOOKS — fire while the form is openon_loadRuns right after the row's data is loaded — useful for refreshing a lookup or prefetching related rows.on_saveRuns after the dialog's main update/insert succeeds — chain extra writes on the same row.TOOLBAR — buttons above the tableactionsRun independently, no row context (or the currently-selected row when one is highlighted).+ Add toolbar actionROW HOOKS — fire after a row is mutatedon_insertAfter a row is inserted (dialog Save or inline grid Save).on_update · on_deleteAfter an edit / a delete. ParamBinds resolve against the new / deleted row's values.
GroupHooksWhen they fireParamBind resolves against
Dialog hookson_load, on_save, on_cancelWhile the form is open.The live form state.
Toolbaractions (screen-level)The user clicks a toolbar button.The currently-selected row (empty if none).
Row hookson_insert, on_update, on_deleteAfter 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).

TypeWhat it doesKey fields
run_queryExecute a connector query (writable or read).connector, query, params (ParamBinds).
call_apiCall an HTTP / API connector endpoint.connector, endpoint, params.
navigateOpen another screen as a modal, narrowed by ParamBinds.connector, screen, params.
set_fieldChange a dialog field's value.field, value (literal or source from another field).
confirmPrompt the user for confirmation; block the chain on Cancel.title, message, confirm_label.
notifyShow a toast / banner.level (info / warning / error / success), message.
refreshRe-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.

TypeWhat it does
chainA 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.
ifConditional branching inside a chain — runs then actions when the condition holds, else actions otherwise.
loopIterate over an array (typically the result of a previous run_query) and run the body for each item.
returnBind 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:

Wheresource 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_loadThe just-loaded form state.
Dialog on_saveThe form state at submit time (after the main write succeeded).
Dialog on_cancelThe form state when the user pressed Cancel.
Row on_insert / on_updateThe new row's values.
Row on_deleteThe deleted row's values.
Inside a chainThe 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:

FieldNotes
idUnique within the action list. Used as the chain-context key (<id>.first_row.<col>).
typeDropdown — switches the available fields below.
TargetThe query / endpoint / screen the action operates on (label depends on type: Query for run_query, Endpoint for call_api, Target query for navigate).
ParamsList of ParamBinds. Click + Add binding to add one.
stop_on_errorWhen false, the chain continues even if this action fails. Default true.
promptOptional 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

MistakeSymptomFix
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