> For the complete documentation index, see [llms.txt](https://docs.frayme.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.frayme.ai/guides/state-and-actions.md).

# State and actions

How interactions on a Frayme-rendered UI resolve — most locally in the browser, the rest dispatched to handlers you control.

## The 90/10 split

Roughly 90% of interactions resolve entirely inside the renderer at zero latency and zero cost: typing into a bound input writes state via `$bindState`, tabs switch, filters filter, visibility toggles. None of that reaches your code.

The remaining \~10% are **spec-bound named actions** — an element's `on` block binding a canonical event verb to an action name:

```json
{
  "type": "Button",
  "props": { "label": "Approve" },
  "on": { "commit": { "action": "approveRefund", "params": { "amount": { "$state": "/amount" } } } }
}
```

When that fires, the renderer resolves the action through a dispatch pipeline and hands it to one of four **kinds** of handler.

## `onDynamicAction`: the agent sink

The simplest wiring — every forwarded action lands in one callback:

```tsx
import { FraymeRenderer, type DynamicActionEvent } from '@frayme/runtime/react';

<FraymeRenderer
  spec={spec}
  onDynamicAction={(event: DynamicActionEvent) => {
    // event.action        → "approveRefund"
    // event.params        → { amount: 420 } — $state refs already resolved to live values
    // event.event         → "commit" — the canonical verb that fired
    // event.state         → full live state snapshot at fire time
    // event.generation_id → correlates back to the compose that built this UI
    sendToAgent(event);
  }}
/>;
```

`DynamicActionEvent` carries everything a host needs without re-deriving it: the resolved params, the fired verb, the complete state snapshot, and the generation id.

## Params resolution

By the time your handler runs, params are plain values:

* `{ "$state": "/path" }` params are resolved against live client state.
* `{ "$item": "field" }` params (rows in a repeated list) are dereferenced to the **value** for the pressed row — you receive `"claimId": "RC-2214"`, never a state path.
* Literal params pass through untouched.

## Action kinds

Instead of a `switch` in `onDynamicAction`, you can map each action name to a declarative kind via the `actions` prop:

```tsx
import type { FraymeActionMap } from '@frayme/runtime';

const actions: FraymeActionMap = {
  // local — deterministic client-side handler (bare function is shorthand)
  copyLink: (params) => navigator.clipboard.writeText(String(params.url)),

  // recompose — regenerate the canvas in place, carrying the current spec
  showBreakdown: { kind: 'recompose', prompt: 'Expand into a per-line cost breakdown' },

  // agent — forward to onDynamicAction (the default kind)
  approveRefund: { kind: 'agent' },

  // deny — render the control inert
  deleteAccount: false,
};

<FraymeRenderer
  spec={spec}
  actions={actions}
  onDynamicAction={sendToAgent}
  compose={frayme.compose}          // enables the recompose kind
  onRecompose={(next) => setSpec(next)} // where the recomposed spec lands
/>;
```

| Kind        | What it does                                                                                                            | Requires                        |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
| `local`     | Runs your function in the browser.                                                                                      | Nothing                         |
| `recompose` | Calls compose with the current spec as `prior_spec` and morphs the canvas via `onRecompose` (a state-preserving merge). | `compose` + `onRecompose` props |
| `agent`     | Forwards the enriched event to `onDynamicAction`.                                                                       | A handler                       |
| `host`      | postMessages to a host bridge (MCP/embed surfaces inject it).                                                           | `hostTransport`                 |
| `false`     | Explicit deny — the control renders inert.                                                                              | Nothing                         |

Every kind degrades safely: `recompose` without a `compose` client, or `host` without a transport, forwards to `onDynamicAction` instead of throwing.

## Who routes: the spec or you

* **You pass nothing** — the spec drives. The server writes handlers into `spec.actions` for declared actions (see [Edits and journeys](/guides/edits-and-journeys.md)); undeclared actions forward to `onDynamicAction`. The spec still can't do anything you didn't enable — each kind is gated on a dep you inject.
* **You pass `actions` (map or `string[]` allow-list)** — you are the sole router. `spec.actions` is ignored, and any name you didn't list fails closed (renders inert), unless you opt unmapped names into a blanket sink with `defaultActionKind: 'agent' | 'host'`.

```tsx
// Allow-list form: only these two names ever reach your handler
<FraymeRenderer spec={spec} actions={['approveRefund', 'exportCsv']} onDynamicAction={sendToAgent} />
```

## Handler context

`local` handlers (and `recompose` prompt functions) receive a context alongside params:

```tsx
const actions: FraymeActionMap = {
  applyDiscount: {
    kind: 'local',
    run: (params, ctx) => {
      ctx.setState('/total', Number(ctx.getState().total) * 0.9); // write state by JSON Pointer
      // ctx.getState() — live snapshot · ctx.getSpec() — current spec
      // ctx.signal     — aborts if the same action fires again (re-entrancy guard)
    },
  },
};
```

Firing the same action again aborts the previous in-flight dispatch via `ctx.signal` — check it before applying async results.

## Per-action confirmation

Any spec action binding may carry a `confirm` block; the renderer pauses execution and shows a Frayme-styled modal before dispatching:

```json
{ "commit": { "action": "deleteRow", "confirm": { "title": "Delete row?", "message": "This cannot be undone." } } }
```

No per-component code — it works on every component.

## Next steps

* [Edits and journeys](/guides/edits-and-journeys.md) — round-trip an action into the next compose
* [Data binding](/guides/data-binding.md) — how `$state` and `$bindState` connect elements to state


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.frayme.ai/guides/state-and-actions.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
