Skip to main content
A hook fires at a moment in a component’s life: when the instance is created, or when one of its views opens. The component fetches its own data at that moment. Think React lifecycle. A component fetches on mount, and the framework decides when mount happens. Here the platform owns the fire points, and a hook is data like everything else in rx/. You declare the calls. The platform runs them.

When you need one

Two situations, and both are data the workflow did not send. The data belongs to one view. A card in a grid shows a title and a line of description. Its detail view needs the whole record: body copy, sections, call to action. Loading that for the grid would fetch twelve documents to draw twelve thumbnails. The data lives somewhere else. A live rating, a stock level, a venue lookup. It changes on its own schedule, so the component asks for it when it renders.

The two moments

A view is a state, and the manifest key says layouts. Both are true, and they are the same word. A component’s root is a Switch on defaultState, and each case includes the face file named after it. State course renders layouts/course. So layouts: [ course ] scopes the hook to the state named course, and the file it renders happens to carry that name too. The distinction matters more than it looks. A card and its detail view are one instance: a course in a grid and the same course expanded differ only by which layout is active. So if a detail view’s content were fetched at onStart, opening a grid of twelve cards would fetch twelve full documents to render twelve thumbnails. onEnterView fetches only what somebody actually opened.

Declaring one

The manifest opts in. Nothing runs that the manifest did not name.
manifest.yaml
Three fields:
  • phase: which moment, onStart or onEnterView.
  • layouts: which views wake it. onEnterView only. Omit it and every layout fires.
  • handler: what runs. Either a platform-ready handler, or your own, in which case <handler>.yaml sits beside the manifest.
A hook is named for what it does, not for when it fires. fetchPlaceDetails says the job; onEnterView already said the moment. It also means one component can want two different jobs at the same moment without a naming collision. Nothing to wire up. A component changes its own view by writing defaultState (the close button on a focused card writes list, the card in the list writes focus). That write is the event: the platform reports it, and a hook scoped to the entered layout runs. No signal to author, no extra action to chain, nothing to remember. If your card already changes its own view, it already emits the event.

Platform-ready handlers

Some handlers would be identical in every project, so the platform ships them and a component just points at one. No file, no code, no copy-paste.

getDetail

Fill this card with its own full record. A search result is a summary. getDetail fetches the long-form copy behind it: the body of a product or service page, and the editorial fields around it. It reads the instance’s universal_id, fetches that item from the dictionary, and merges these fields into the card: Prop names match by name, exactly like every other hydration path. A field the record does not carry is left alone, so the card keeps whatever the search row already gave it. Fields outside this list stay behind: crawler bookkeeping such as timestamps, paths and structured markup never reaches a card. The card carries the id and the platform dereferences it, so authored copy never travels through a model to reach a card.
Two things make this safe to use everywhere:
  1. It can only fetch its own record. The id comes from the platform’s copy of the instance, so a card cannot ask for somebody else’s content.
  2. It fails quietly. No id, an unknown id, a slow engine: the card keeps whatever the search row already gave it. A thinner detail view, never a broken one.

Beyond the built-ins: declare the calls

When no platform handler fits, a hook makes its own requests. It declares them the way a node does. Name your own handler, and the platform runs the calls you put in <handler>.yaml. Here is a whole component that does it. RestaurantCard streams into the conversation knowing only what the search gave it. Open it, and it fills its own live details from a maps API. Three parts, no code:

manifest.yaml

manifest.yaml
handler: fetchPlaceDetails is not a platform handler, so the platform runs fetchplacedetails.yaml from this folder. layouts: [ focus ] is why the card in the list never calls anything: only entering the focus face wakes the hook. allowedHosts is not optional. A hook that makes requests without it is a lint error, not a warning. Deny by default, and the refusal names the component. Credentials are named here and fetched server-side by type. The key never sits in the folder. It never sits in the definition either.
A credential appears where you enter it, and leaves when nothing needs it. The definition ships with a node package. Install the package and the credential shows up in Canvas under Credentials, ready to fill in. Uninstall it and the type is retracted again. A component names one of those definitions, so naming one that no package defines is a lint error rather than a card that silently renders empty.

fetchplacedetails.yaml

The file is named after the handler, lowercased.
fetchplacedetails.yaml
Two keys. calls is the same list a node’s api/run.yaml holds, and it runs through the same function. A hook inherits the host allowlist, the credentials, the retries, the timeouts and the transports. returns projects the results into partial props. They merge into the instance by name, like any other hook. The real component takes a second call for photos, and guards it with when: "return !!(...).place_id" so it only runs when the first call found a place. Calls run in order, and each one can read the ones before it through calls.<name>.
The filename is the handler, lowercased. handler: fetchPlaceDetails looks for fetchplacedetails.yaml. macOS ignores the case difference and Linux does not, so a camel-cased file works on a laptop and fetches nothing in a container.
A component describes what it shows. The calls describe how to fetch something. Neither has to grow into the other, which is how rx/ stays free of code.
A raw <phase>.js file beside the manifest is the older form, and the one case still named for the phase. It still runs, for handlers that predate declared calls. It is not the pattern to copy. A script does its own fetch to any host, with a key read from the environment, and the calls runtime settles both of those questions once.

What the platform guarantees

A hook runs on the server, so the platform is strict about it. Fire points are platform-owned. You add handlers, never moments. The phase set is closed, so nothing fires that the platform did not define. The hosts are locked. A hook reaches only the hosts its own manifest lists, and a component that declares none reaches nothing. A definition cannot execute code, but it could name any URL, so the allowlist is the boundary that makes a data hook safe. A call carrying a credential must also be HTTPS, so a key never travels in clear text. The credential stays in the platform. The manifest names it by type. The platform fetches it server-side from encrypted storage and attaches it to the call. It is never in the folder, never in the definition, and never on the client. The client asks for nothing. When a view opens, the client says only β€œthis instance entered this layout”. The platform answers every other question itself: which component that is, whose session owns it, what props it holds. Inputs are re-derived server-side and never taken from the message. An instance the session does not own runs nothing at all. A phase fires once per instance. Open a detail view, close it, open it again: one fetch. Body copy does not change mid-conversation. Enrichment streams in. The view opens immediately with what the card already has, and the fetched fields fill in when they arrive. No blocked render, no spinner.

Lint catches the mismatches

The two halves, what the manifest declares and what actually runs, have to agree. Each of these is an error, not a silent no-op:

Which one do I need?

And if the answer is β€œthe model already knows it”, that is not a hook. Content the model authored is a prop, fed by the workflow like any other.
Next: 07, Studio, see it, test it, on every channel.