Codelab
Behavior

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):

TargetfunctionIdWhere it lives
Primitiveslug, e.g. "set-property"Code. Discover with list_primitive_functions.
Store actionthe action's idstores.<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).

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

functionIdContextParamsNotes
delayclientmillisecondsWait before the next link in a store action.
browser-alertclientmessageBlocking alert().
logclientlabel, valueconsole.log for wiring.
toastclienttitle, description?, type?, timeout?, id?, anchor?Repeating id replaces; anchor is an Element binding.
code-actionclientcode, async?JS expression; its value becomes {{event}} for onSuccess. Reach browser APIs as {{js.navigator.clipboard.writeText(…)}}.
abort-controller-createclientstore, propertyWrites a live AbortController onto the field. After .abort() it is spent — call again.
http-requestserver, publicurl, 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-loginserver, publicemail, passwordWrites {{session.*}} (pending / user / error). Do not declare a session store.
supabase-signupserver, publicemail, passwordSame session seam.
supabase-logoutserver, public(none)Clears the session cookie.
supabase-queryserver, publictable, operation?, values?, match?, filters?, columns?, single?, maybeSingle?, order?, limit?, rangeFrom?, rangeTo?, count?, onConflict?, returning?, guestToken?, plus optional response captureRuns 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):

ExpressionMeaning
{{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.

On this page