Hooks
Updated 10 minutes ago • May 22, 2026
Hooks are the public node-level runtime API. Import them from kortyx.
Use them for four things:
- run model reasoning
- pause for human input
- keep short-lived runtime state
- stream UI-ready structured data
Quick Selection
- Need an LLM call in a node:
useReason(...) - Need manual human-in-the-loop input:
useInterrupt(...) - Need state local to one node execution flow:
useNodeState(...) - Need state shared across nodes in the same run:
useWorkflowState(...) - Need request metadata inside a node:
useRuntimeContext(...) - Need structured UI updates in the stream:
useStructuredData(...)
Structured Streaming Mental Model
Think about structured streaming as a second channel beside normal assistant text:
text-*chunks are text you render as textstructured-datachunks are object updates you render as UI statestreamIdidentifies one logical structured streamkindtells the client how to apply the update
In practice:
- use
useReason({ structured })when the model owns the object - use
useStructuredData(...)when your node logic owns the updates
useReason(...)
Use this for the main model call in a node.
Typical use:
- schema-constrained outputs
- optional interrupt flow (
interrupt) - structured stream payloads (
structured)
Emission vs ui.message
emit controls whether useReason(...) publishes model output into the stream while the model is running. ui.message is a node return value that emits a final message after the node finishes.
Do not return ui: { message: result.text } from the same node when useReason({ emit: true }) already streams the answer. That sends the same assistant text through two channels.
Use one of these patterns:
Good to know: Return
datafor values later nodes need. Returnui.messageonly for assistant text the client should receive as a final message chunk.
Example: stream an email draft as JSON
This is the most useful structured-streaming shape:
- one field grows as text
- one field grows as an array
- the full validated object arrives at the end
What useReason(...) returns
useReason(...) returns more than final text. You can also inspect normalized provider metadata:
What each field means:
text: final assistant textoutput: parsed and validated object whenoutputSchemasucceedsraw: provider-native payload for debuggingusage: normalized token usage when the provider exposes itfinishReason: normalized stop reasonproviderMetadata: provider-specific metadata that does not fit the shared top-level contractwarnings: compatibility or unsupported-feature warnings surfaced by the providerinterruptResponse: final human response when you use interrupt mode
Good to know: In interrupt flows, Kortyx aggregates
usage,warnings, andproviderMetadataacross the first pass and continuation pass. Runtime token usage is also accumulated intostate.runtime.tokenUsage.
Common model call options
These are the main cross-provider options you can pass to useReason(...):
If a provider cannot fully support one of these generic options yet, it should surface a warning instead of silently ignoring it.
What happens:
- the model still generates one JSON object
- Kortyx watches the streamed JSON
- all incremental updates from that one
useReason(...)call share onestreamId - when
subjectbecomes complete, Kortyx emitsstructured-datawithkind: "set" - when
bodygrows, Kortyx emitsstructured-datawithkind: "text-delta" - when
bulletsgains finished items, Kortyx emitsstructured-datawithkind: "append" - when the whole object validates, Kortyx emits one
structured-datachunk withkind: "final"
That means a client can start rendering the email body and bullets before the final object arrives.
Fields that are not declared in structured.fields are available when the final object arrives.
Default behavior
If you provide structured but do not provide fields, useReason(...) emits one final structured object when parsing and validation succeed.
That is the default and simplest path:
Current incremental streaming limits
Today, useReason({ structured }) incremental field streaming supports:
setfield paths- string field paths as
text-delta - array field paths as
append - non-interrupt flows only
That means:
- dotted paths such as
draft.bodyortable.rowscan be used byuseReason(... structured.fields ...) - numeric path segments can target array indexes, such as
sections.0.body *matches one object key or array index segment, such asassessment_points.*.criteria_label- empty field keys are rejected
- if you combine
useReasonwithinterrupt, you still get structured output when a valid object exists, but incremental field streaming is not combined with interrupt mode today - when
outputSchemaorinterruptis present,useReasonsuppresses normal assistant text chunk streaming because the runtime is parsing and validating structured output
In practice, expect structured-data and interrupt events in those cases, not text-delta.
Good to know:
useReason(...)validates the final object againstoutputSchema, but incremental chunks are enforced only at the path and operation level. If you need per-update schema checks before the final object, emit manualuseStructuredData(...)chunks withvalueSchema,itemSchema, ordataSchema.
When to use useReason({ structured })
Use it when:
- the model result itself is the UI object you want to render
- you want the final object validated against
outputSchema - a string field or array field should become visible before the full object is done
Use useStructuredData(...) instead when:
- the updates come from app logic rather than the model
- you need precise control over when fields are emitted
- you want to emit
set,append,text-delta, orfinaldirectly
useInterrupt({ request, ...schemas })
Use this when you want fully manual interrupt payloads without LLM-generated request shaping.
Return:
stringfortextandchoicestring[]formulti-choice
Use stable id values for interrupts in nodes that can replay or contain multiple interrupt calls.
useNodeState and useWorkflowState
Node-local state:
Workflow-shared state:
State Lifetime and Limits
useNodeState:
- persists for repeated executions of the same node inside one run
- is node-local only
useWorkflowState:
- persists across nodes and workflow transitions within the same run
- restores on interrupt resume for that run
Across messages and sessions:
- hook state is not a long-term session store
- a new chat request starts a new run with fresh hook state
- for cross-request persistence, call your own DBs or service clients from node code
Durability and practical limits:
- hook state is tied to runtime checkpoint persistence
- in-memory framework adapter is process-local and not restart-safe
- Redis framework adapter can restore across restarts until TTL expiry
- keep hook state small and JSON-serializable
useRuntimeContext(...)
Use this when node code needs request metadata passed from the route, such as selected thread id, locale, or server-approved auth context.
See Runtime Context for the full client-to-route-to-node flow and security boundary.
useStructuredData(...)
Use this when your node wants to emit UI updates directly.
The API is intentionally simple:
kind: "set"sets one field at a pathkind: "append"appends items to an array fieldkind: "text-delta"appends text to a string fieldkind: "final"publishes the completed object
If you omit kind, useStructuredData(...) defaults to final.
Example: drive an email composer UI yourself
Use this pattern for:
- email or document composers
- tables that gain rows over time
- growing arrays such as job cards or search results
- progress panels and dashboard state
streamId and id
streamIdis the client-facing identity for one structured stream- keep it stable across related updates so the client knows which object is being updated
idis optional app metadata you may also want on the chunk
If you do not pass streamId, Kortyx generates one. That is fine for one-off final payloads, but for multi-step updates you usually want to pass a stable streamId yourself.
In useStructuredData(...), path uses dot notation such as table.rows or draft.body. append should target an array field, and text-delta should target a string field. useReason(... structured.fields ...) supports the same nested path notation plus single-segment * patterns for model-generated keys.
Manual structured updates can build nested objects incrementally, but once a path holds a string, number, boolean, or other non-container value, later chunks cannot treat that same location as an object or array.
On resume, node code starts again from the top. useReason continues from its internal checkpoint, but code before useReason can run again. Keep useReason as the first meaningful operation and guard pre-useReason side effects with useNodeState.
For chunk shapes and recommended client reducers, see Stream Protocol.