Function
A Type.Function prop slot, the FunctionModel stored on it, and published dispatch by FunctionSlot.
There is no separate Function entity to create. Function in the product is a callable prop slot (Type.Function) plus the FunctionModel persisted on it. Published dispatch addresses that slot as a FunctionSlot { elementId, field } so the visitor never sends the binding — the server reloads the owner's template (packages/modules/action/kernel/src/function-runtime.types.ts).
FunctionModel
The value stored on the slot — and on each step of a store Action — is:
{
"functionId": "set-property",
"instanceParams": {
"store": "rsvp",
"property": "modalOpen",
"value": "{{event}}"
}
}(packages/modules/action/kernel/src/function.model.ts.) functionId names a primitive slug, a store action id, or "<hookId>.<method>". instanceParams is the call-site payload; custom store actions with no declared params ignore it.
Optional when is an expression evaluated at dispatch against the same context as the params. Falsy skips the call (and, for a store action, its whole chain) — not an error.
{
"functionId": "set-property",
"instanceParams": {
"store": "ui",
"property": "wasPressed",
"value": true
},
"when": "{{event.pressed && !stores.ui.wasPressed}}"
}A whole-binding string "{{…}}" in instanceParams or when canonicalizes to { "_tag": "Expression", "source": "{{…}}" }. Mixed text ("Hello {{stores.user.name}}") stays a string and interpolates.
Bind it with update_props on any event prop (onClick, onChange, onValueChange, onOpenChange, onSubmit, …):
{
"elementId": "<button-id>",
"commands": [
{
"op": "set",
"key": "onClick",
"value": {
"functionId": "set-property",
"instanceParams": {
"store": "rsvp",
"property": "modalOpen",
"value": true
}
}
}
]
}One call per element slot
An element event holds one FunctionModel, matching React's onClick={fn}. Sequences belong on a store action's functions[], pointed at from N slots (packages/modules/action/kernel/src/function.model.ts). A hook config slot (useForm.onSubmit, useEffect.effect / cleanup) is the same shape (packages/modules/hook/client/src/registry/use-form.metadata.ts).
Do not put a JavaScript body on an element. A body is opaque code with a declared signature; it lives as a store action whose chain is a single code-action (packages/modules/action/core/src/domain/action.model.ts). The element only stores { functionId } plus call-site params.
FunctionSlot
{ "elementId": "el_submit", "field": "onClick" }The renderer threads this address into the wrapped handler (packages/modules/renderer/core/src/application/prop-resolvers.application.ts). Inner links — store-action steps, onSuccess / onError — have no slot.
Published dispatch (packages/modules/action/core/src/application/run-published-function.write.schema.ts):
{
"slot": { "elementId": "el_submit", "field": "onClick" },
"data": { "stores": {}, "page": {}, "session": {}, "hooks": {} }
}The visitor supplies identity + opaque namespace values. The server loads element.props[field], refuses a missing FunctionModel, and runs only a public server primitive (packages/modules/action/server/src/services/run-published-function.service.ts). appId (and therefore {{env.X}}) is derived from the element's page, never from the visitor. Component-scoped elements are not on this path.
The other published identity is a hook id — useQuery reloads its persisted config and dispatches http-request or supabase-query the same way.
Public server primitives (access: public in packages/modules/action/core/src/domain/execution-access.ts): http-request, supabase-login, supabase-signup, supabase-logout, supabase-query. Bind those directly on the element (or hook) slot. Nested inside a store action they have no FunctionSlot and dispatch throws. Chain navigation / set-property with onSuccess on that same top-level binding instead — those continuations run on the client after the server primitive settles (packages/modules/action/client/src/function.runtime.ts).
Client primitives (set-property, navigate, toast, …) never cross that RPC; they run locally from the FunctionModel already on the prop.
Wrap vs identity
What the renderer does with the stored FunctionModel follows the slot, not a second value shape (packages/modules/schema/kernel/src/schema.field.ts):
| Slot | Declaration | Runtime |
|---|---|---|
Event handler (onClick, onValueChange, …) | identity omitted (default) | Wrap (event) => execute(binding, event, props, slot). Result goes to a store or the next link, not back to the host. |
Host reads the return (filter, itemToStringLabel, validate, …) | identity: true and usually async: false | Pass the compiled store callable as-is. Do not wrap — a wrapper settles to a Promise the host reads as truthy. |
An identity / async: false slot must bind a store action that is a single synchronous code-action. A primitive has no compiled callable; binding set-property there is rejected (packages/modules/registry/core/src/domain/sync-action-field.validator.ts). Hook methods are always wrapped as handlers, even on an identity slot.
Absent params on the slot means React's single event argument. Declared params on a wrapped event field are type-level today; {{event}} is still the stored spelling for that argument.
Hook and effect slots
These config keys are also Type.Function — persist a FunctionModel, not a body:
{
"alias": "rsvpForm",
"kind": "useForm",
"config": {
"fields": [{ "name": "email", "type": { "kind": "text" } }],
"onSubmit": {
"functionId": "set-property",
"instanceParams": {
"store": "rsvp",
"property": "lastSubmission",
"value": "{{event.value}}"
}
}
}
}{{event.value}} is the submitted form values after validation. update_hook(config) wholesale-replaces the config object — list_hooks first, then send fields and onSubmit together.
useEffect effect / cleanup are the same FunctionModel slots. Cleanup is where cancellation composes (abort a useRef-held controller), matching React's return () => ….