Action
Named store chains and code-shipped primitives that run when a Function slot fires.
An Action is a named, ordered chain of calls on a store. The same call shape also names a primitive (set-property, navigate, …) or a hook method (<hookId>.setFieldValue). You never put a sequence on an element — an event slot holds one call; multi-step lives on the store.
The call itself is a FunctionModel { functionId, instanceParams?, when? } (packages/modules/action/kernel/src/function.model.ts). The named chain is an ActionModel { id, name, functions, params?, returns? } (packages/modules/action/core/src/domain/action.model.ts). Bind a FunctionModel on a Function slot with update_props (Props).
{
"functionId": "set-property",
"instanceParams": {
"store": "rsvp",
"property": "modalOpen",
"value": true
}
}functionId is one of three things (packages/modules/action/kernel/src/function-id.ts, resolved in packages/modules/action/client/src/function.runtime.ts):
| Target | functionId | Where it lives |
|---|---|---|
| Primitive | slug, e.g. "set-property" | Code. Discover with list_primitive_functions. |
| Store action | the action's id | stores.<name>.actions[]. |
| Hook method | "<hookId>.<method>" | The hook row's id, not its alias. |
Unknown ids throw Unknown action id: … at dispatch.
Primitives
Code-shipped ids (packages/modules/action/kernel/src/primitive-function-id.ts). Param keys are persisted in instanceParams (packages/modules/action/core/src/domain/primitive-function.read.schema.ts); the Field catalogs are packages/modules/action/core/src/domain/primitive.write.registry.ts.
Client primitives run in the browser. Server primitives with access: public run from the published app by reloading the owner's persisted binding — see Function.
set-property
Write one store field. value may be a literal or a {{…}} expression.
{
"functionId": "set-property",
"instanceParams": {
"store": "rsvp",
"property": "modalOpen",
"value": "{{event}}"
}
}{{event}} is the first argument the host passed — for onValueChange / onOpenChange / onCheckedChange that argument is the new value. Native onChange is usually {{event.target.value}}.
Declare every field you write, with a defaultValue. An undeclared field resolves to undefined and flips controlled inputs. A computed field (editable: false + an expression defaultValue) is derived on read; do not target it with set-property (packages/modules/schema/kernel/src/schema.type-expression.ts).
navigate
Change page in the rendered app. Destination search is {{page.search.*}} there; path params are {{page.params.*}}.
{
"functionId": "navigate",
"instanceParams": {
"pageId": "page_home",
"search": { "token": "{{page.search.token}}" },
"routeParams": { "id": "{{stores.events.selectedId}}" }
}
}Empty / null route-param values are dropped so they do not stringify as "undefined" (packages/modules/action/core/src/domain/primitives/navigate.primitive.ts).
Hook methods
functionId is "<hookId>.<method>" — the hook id from list_hooks, not hooks.<alias>. Expressions still use the alias: {{hooks.rsvpForm.values.email}}.
instanceParams are keyed by the method's declared parameter names, then passed positionally in declaration order (packages/modules/action/core/src/domain/hook-lookup.ts). A method with params: [] is called with nothing — handleSubmit is that shape so a click event cannot masquerade as submit meta.
{
"functionId": "<rsvpForm-hookId>.setFieldValue",
"instanceParams": {
"name": "email",
"value": "{{event.target.value}}"
}
}useForm methods: handleSubmit, handleChange, setFieldValue, reset. useQuery: refetch. Post-submit side effects belong on the hook config's onSubmit FunctionModel, not on the Form element's DOM onSubmit (packages/modules/hook/client/src/registry/use-form.metadata.ts).
Other primitives
functionId | Context | Params | Notes |
|---|---|---|---|
delay | client | milliseconds | Wait before the next link in a store action. |
browser-alert | client | message | Blocking alert(). |
log | client | label, value | console.log for wiring. |
toast | client | title, description?, type?, timeout?, id?, anchor? | Repeating id replaces; anchor is an Element binding. |
code-action | client | code, async? | JS expression; its value becomes {{event}} for onSuccess. Reach browser APIs as {{js.navigator.clipboard.writeText(…)}}. |
abort-controller-create | client | store, property | Writes a live AbortController onto the field. After .abort() it is spent — call again. |
http-request | server, public | url, method?, headers?, body?, responseStore?, responseProperty? | Secrets as {{env.X}} resolve on the server. Result { status, data } — 2xx is success, 4xx/5xx fires onError with the same shape. |
supabase-login | server, public | email, password | Writes {{session.*}} (pending / user / error). Do not declare a session store. |
supabase-signup | server, public | email, password | Same session seam. |
supabase-logout | server, public | (none) | Clears the session cookie. |
supabase-query | server, public | table, operation?, values?, match?, filters?, columns?, single?, maybeSingle?, order?, limit?, rangeFrom?, rangeTo?, count?, onConflict?, returning?, guestToken?, plus optional response capture | Runs as the signed-in user (or anon) under RLS, only against this app's project. |
list_primitive_functions returns the live Field catalog per id. Server primitives optionally capture { ok, status, data } into responseStore / responseProperty after a successful settle (packages/modules/action/client/src/function.runtime.ts).
Store actions
A store holds actions: ActionModel[]. One action is a reusable chain; its functions array is the only place a sequence lives (packages/modules/store/core/src/domain/store.model.ts).
{
"id": "act_toggleFaq",
"name": "toggleFaq",
"functions": [
{
"functionId": "set-property",
"instanceParams": {
"store": "ui",
"property": "faqOpen",
"value": "{{!stores.ui.faqOpen}}"
}
}
]
}Bind it by id, not name:
{ "functionId": "act_toggleFaq" }Optional params (a Field list) and returns (a FieldType) are the chain's signature. Call-site instanceParams resolve first, then inner links read them as {{params.<name>}}. Inside the chain, {{this.<field>}} is the owning store's live state (packages/modules/action/core/src/domain/function-context.model.ts).
The chain awaits each link. Its value is the last link's settled value. A later link may read what an earlier one wrote ({{event}}, a responseStore capture).
A synchronous value the host reads at the call site (filter, itemToStringLabel, validate) must be a single code-action whose body is not declared async: true (packages/modules/action/kernel/src/function.read.domain.ts). That compiled callable is also reachable as {{stores.<name>.<actionName>(…)}}. Anything richer (a primitive, a multi-link chain, a body marked async) dispatches through the runtime and settles to a Promise.
update_store wholesale-replaces fields and actions. list_stores first (pass storeId for the full arrays), append in memory, send the whole array.
onSuccess / onError
Any primitive binding may carry reserved continuation slots in instanceParams. They are not primitive params: the runtime strips them before dispatch and never sends them to the server (packages/modules/action/core/src/domain/primitive-execution.ts).
Exactly one fires after settle:
onError— the primitive threw, or it returned a soft failure (Result.err/{ ok: false }).onSuccess— otherwise.
Each value is a nested FunctionModel (client, server, or store action). The outcome is {{event}} for the handler. Auth primitives re-read {{session.*}} after they write it, so a login's onSuccess sees the new user.
{
"functionId": "supabase-login",
"instanceParams": {
"email": "{{stores.login.email}}",
"password": "{{stores.login.password}}",
"onSuccess": {
"functionId": "navigate",
"instanceParams": { "pageId": "page_home" }
}
}
}Unhandled soft failure stops a store-action chain — wire onError to continue. A handler that itself fails is that handler's failure, not the original primitive's.
Expressions on a call
instanceParams values may be literals or {{…}}. A whole-binding string ("{{event}}") canonicalizes to { "_tag": "Expression", "source": "{{event}}" } (packages/modules/action/kernel/src/function.write.domain.ts). Nested objects and arrays resolve too (packages/modules/action/core/src/domain/function-params.domain.ts).
On dispatch, these namespaces are in scope (packages/modules/expression/core/src/domain/expression.namespaces.ts):
| Expression | Meaning |
|---|---|
{{event}} | Handler argument, or the previous link / continuation's value. |
{{stores.<name>.<field>}} | Live store state. |
{{hooks.<alias>.<key>}} | Hook output (values, data, matches, …). |
{{this.<field>}} | Owning store, inside a store action. |
{{params.<name>}} | Declared action inputs, inside a store action. |
{{session.*}} / {{page.*}} | Ambient viewer / route. Display only — authorization is the session cookie + RLS. |
{{env.X}} | Server-only secrets; stay literal on the client so the server resolves them. |
{{js.*}} | globalThis (clipboard, localStorage, …), event-time on the client only. |
{{props.*}} | Enclosing component instance props, including host callbacks. |