design/. You declare the calls, and the platform runs them.
When you need one
Two situations, and both are data the workflow did not send. The data belongs to one state. A card in a grid shows a title and a line of description, while its detail state 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. Which hook you need follows from what the data is:The two moments
The scope key is spelled
layouts, and its entries are state names. A component
declares a state tree, and each state owns the layout that draws it
(Components), so scoping a hook to a state also names the moment its
drawing appears. The names in the list are always states, never layout filenames. The
shipped product card declares layouts: [ page ], and page is a state, whose own
drawing happens to live at layouts/page.
The distinction matters more than it looks. A card and its detail are one instance, and
they differ only by which state is active. So if a detailβ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, and nothing runs that the manifest did not name.manifest.yaml
phaseis which moment,onStartoronEnterView.layoutsis which states wake it, ononEnterViewonly. Omit it and every state fires.handleris what runs: either a platform handler, or your own, in which case<handler>.yamlsits beside the manifest.
fetchPlaceDetails says the
job, and onEnterView already said the moment. It also means one component can want two
different jobs at the same moment without a naming collision.
Two behaviours are worth knowing as you write one. A phase fires once per instance, so
opening a detail view, closing it and opening it again is one fetch. And enrichment
streams in: the state opens immediately with whatever the card already has, and the
fetched fields fill in when they arrive, so there is no blocked render and no spinner to
author.
Nothing to wire up. A component changes its own face by writing state, and that write
is the event. The platform reports it, and a hook scoped to the entered state runs. There
is no signal to author and no action to chain: if your card already changes its own state,
it already emits the event.
Platform handlers
Some handlers would be identical in every project, so the platform ships them and a component points at one.getDetail fills a card with its own full record. A search result is a summary, and
getDetail fetches the long-form copy behind it. It reads the instanceβs universal_id,
fetches that item from the dictionary, and merges the recordβs editorial fields into the
card. Interface data lists every field that arrives, and the
fields the card already had before the hook ran.
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, and
crawler bookkeeping such as timestamps and paths never reaches a card at all.
Two things make it safe to use everywhere:
- 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.
- It fails quietly. No id, an unknown id or a slow engine leaves the card with whatever the search row gave it. A thinner detail view, never a broken one.
Your own calls
When no platform handler fits, a hook makes its own requests, declared the way a node declares them. Name your own handler, and the platform runs the calls in<handler>.yaml.
A restaurant card 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:
handler: fetchPlaceDetails is not a platform handler, so the platform runs
fetchplacedetails.yaml from this folder. layouts: [ page ] is why the card in the list
never calls anything, because only entering the page state wakes the hook.
Two keys do the work. calls is the same list a nodeβs api/run.yaml holds, run
through the same function, so a hook inherits the host allowlist, the credentials, the
retries, the timeouts and the transports. returns projects the results into partial
props, which merge into the instance by name like any other hook. Calls run in order, and
each one can read the ones before it through calls.<name>, so a second call can be
guarded with when and only run when the first found something.
allowedHosts is not optional. A hook that makes requests without it is a lint error
rather than a warning. A definition cannot execute code, but it could name any URL, so the
allowlist is the boundary that makes a data hook safe. Deny by default, and the refusal
names the component.
A call carrying a credential must be HTTPS, and the platform attaches the key server-side,
so it is never in the folder, never in the definition and never on the client.
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.<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, but 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.
Next steps
Studio
Watch a hook fire against a live universe.
manifest.yaml
Where
lifecycle is declared, with every other manifest field.
