API Reference
Complete API reference for all RilayKit packages.
API Reference
Package mains are React-free (RSC/server-safe): import builders, validators, and schema compilers from @rilaykit/core, @rilaykit/forms, @rilaykit/workflow, @rilaykit/agent (or the all-in-one rilaykit). Import components and hooks from the matching /react entry.
@rilaykit/core
Catalog (ril), validators, conditions, effects, and error classes shared by all packages.
ril
The immutable catalog: registered components, tools, and parts. Every mutation returns a new instance.
import { ril } from '@rilaykit/core';
import { z } from 'zod';
const catalog = ril.create()
.component('email', {
description: 'Email input',
propsSchema: z.object({ label: z.string() }),
renderer: (ctx) => (
<input
aria-label={ctx.props.label}
value={String(ctx.field?.value ?? '')}
onChange={(e) => ctx.field?.onChange(e.target.value)}
onBlur={ctx.field?.onBlur}
/>
),
})
.tool('lookup_company', {
description: 'Fetch company data by SIREN',
inputSchema: z.object({ siren: z.string() }),
})
.part('text', { renderer: ({ part }) => <p>{(part as { text: string }).text}</p> });| Method | Returns | Description |
|---|---|---|
ril.create() | ril<C> | New empty catalog |
.component(type, entry) | new instance | Register a component (entry: name?, description?, propsSchema?, propsJsonSchema?, renderer?, defaultProps?, validation?, meta?, replace?) |
.tool(name, entry) | new instance | Register a tool (description?, inputSchema?, inputJsonSchema?, renderer?, meta?, replace?) |
.part(type, entry) | new instance | Register a message-part renderer (renderer required) |
.use(plugin) | new instance | Apply a RilayPlugin ((r) => r.component(...)...) |
.renderers({ components?, tools?, parts? }) | new instance | Attach renderers to already-registered entries |
.getComponent(id) / .getTool(name) / .getPart(type) | entry or undefined | Lookup |
.getAllComponents() / .getAllTools() / .getAllParts() | arrays | Enumerate |
.hasComponent(id) | boolean | Existence check |
.validateProps(type, props) | PropsValidationResult | Check props against the component's propsSchema |
.validate() / .validateAsync() | string[] / Promise<AsyncValidationResult> | Configuration validation |
.getStats() | { total, components, tools, parts } | Counts |
.clone() / .removeComponent(id) / .clear() | new instance | Immutability utilities |
Registering an existing key throws DuplicateError unless the entry sets replace: true.
ComponentRenderContext
Passed to every component renderer:
interface ComponentRenderContext<TProps = Record<string, unknown>> {
readonly id: string;
readonly props: TProps;
readonly field?: FieldBinding; // value, onChange, onBlur, error?, disabled?, isValidating?, touched?
readonly conditions?: FieldConditions; // visible, disabled, required, readonly
readonly children?: React.ReactNode;
readonly meta?: Record<string, unknown>;
}field.error and conditions.{required,disabled,readonly} carry what WCAG-AA rendering needs (aria-invalid, role="alert", ARIA attributes); field.touched gates error display.
Validators
All built-ins implement Standard Schema (StandardSchemaV1) and mix freely with Zod, Valibot, ArkType, etc.
import {
required, email, url, minLength, maxLength,
pattern, number, min, max, custom, async, combine,
} from '@rilaykit/core';| Validator | Signature |
|---|---|
required | required(msg?) |
email | email(msg?) |
url | url(msg?) |
minLength / maxLength | minLength(min, msg?) / maxLength(max, msg?) |
pattern | pattern(regex, msg?) |
number | number(msg?) |
min / max | min(val, msg?) / max(val, msg?) |
custom | custom<T>(fn, msg?) — sync (value: T) => boolean |
async | async<T>(fn, msg?) — (value: T) => Promise<boolean> |
combine | combine<T>(...schemas) — merge Standard Schemas |
Conditional Logic
when(field) returns a ConditionBuilder:
import { when } from '@rilaykit/core';
when('role').equals('admin').or(when('permissions').contains('write'))
when('address.country').equals('FR') // nested paths workOperators: .equals(), .notEquals(), .greaterThan(), .lessThan(), .greaterThanOrEqual(), .lessThanOrEqual(), .contains(), .notContains(), .in(), .notIn(), .matches(), .exists(), .notExists().
Combinators: .and(condition), .or(condition). Terminals: .build() → ConditionConfig, .evaluate(data) → boolean.
A field with only conditions.required (no validation block) is enforced: submit blocks while the condition holds and the field is empty.
Field Effects
onChange(fieldId, handler) declares reactive field-to-field logic — fieldId is the field being watched, not the field the effect is declared on.
import { onChange } from '@rilaykit/core';
onChange('country', async (value, { setValue, setProps }) => {
setValue('city', '');
setProps('city', { options: await fetchCities(value) });
});| Context method | Description |
|---|---|
setValue(fieldId, value) | Set another field's value (cascades) |
setProps(fieldId, props) | Merge dynamic props into a field |
getValues() | Snapshot of all form values |
getFieldValue(fieldId) | One field's current value |
Protections: cascade depth cap (10), cycle detection, async abort (only the latest handler wins). Effects run regardless of field visibility; a repeatable-template effect watching a global field fans out per live row with row-scoped setValue/getFieldValue.
Errors and Constants
import { RilayError, ValidationError, DuplicateError, NotFoundError } from '@rilaykit/core';RilayError carries code: RilayErrorCode and meta?. Subclasses: ValidationError (VALIDATION), DuplicateError (DUPLICATE), NotFoundError (NOT_FOUND), InvalidSchemaError (INVALID_SCHEMA), ConfigurationError (CONFIGURATION), MaxDepthExceededError (MAX_DEPTH).
FORM_LEVEL_ERROR_KEY ('__form__') and FORM_LEVEL_ERROR_CODE ('FORM_LEVEL') are the reserved bucket/code of the path-keyed error map (see useFormErrors).
@rilaykit/forms
Builder and schema compiler from @rilaykit/forms; components and hooks from @rilaykit/forms/react.
Components
import { Form } from '@rilaykit/forms/react';
<Form of={contactForm} defaults={{ email: '' }} onSubmit={save}>
<Form.Body />
<Form.Submit />
</Form>Form
Compound root (Form.Body, Form.Field, Form.Submit, Form.List). Wraps FormProvider.
| Prop | Type | Description |
|---|---|---|
of | FormConfiguration | form | Built configuration or builder (auto-built) |
defaults? | Record<string, unknown> | Initial values. Live upgrade channel: changing it re-seeds untouched fields; edited fields are immune |
onSubmit? | (data) => void | Promise<void> | Receives only visible fields' values |
onFieldChange? | (fieldId, value, formData) => void | Per-change callback |
onFieldsRemove? | (fieldIds, formData) => void | Fired when repeatable-row removal deletes keys |
onRepeatableOrderChange? | (order) => void | Live row-order mirror |
defaultRepeatableOrder? | Record<string, string[]> | Row order to restore |
instanceId? | string | Owner identity; same config + different instanceId = different form |
conditionValues? | Record<string, unknown> | Read-only extra values conditions may reference (cross-step) |
className? | string | CSS class |
Form.Body / Form.Field / Form.Submit / Form.List
| Component | Props |
|---|---|
Form.Body | children?: ({ rows }) => ReactNode, className? — renders all visible rows by default |
Form.Field | id, config?, disabled?, overrides? (extra props merged in), forceVisible?, className? |
Form.Submit | children?: ReactNode | (({ submitting, submit }) => ReactNode), className? |
Form.List | id, children?: (ctx: FormListContext) => ReactNode, className? — ctx: items, add(), remove(key), move(from, to), canAdd, canRemove |
FormProvider is the lower-level provider (formConfig, defaultValues, same callbacks); useForm() reads its context: formConfig, conditionsHelpers, validateField, validateForm, validateFormLevel, submit.
form (FormBuilder)
import { form } from '@rilaykit/forms';
const contact = form.create(catalog, 'contact') // formId optional
.add({ id: 'email', type: 'email', props: { label: 'Email' } })
.add( // multiple fields in one call = one row
{ id: 'firstName', type: 'input', props: { label: 'First' } },
{ id: 'lastName', type: 'input', props: { label: 'Last' } },
)
.setValidation({
mode: 'onTouched', // when a field FIRST validates (default)
reValidateMode: 'onChange', // re-validation after an error (default)
validate: z.object({ password: z.string().min(8), confirmPassword: z.string() })
.refine((d) => d.password === d.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'], // routed to that field's errors
}),
});| Method | Description |
|---|---|
.add(...fields) / .add([fields]) | Add fields; one call = one row |
.addSeparateRows(fields) | Each field on its own row |
.addRepeatable(id, (r) => r.add(...).min(n).max(n).defaultValue(v)) | Repeatable group (strictly one level — no nesting) |
.setValidation(config) | Form-level FormValidationConfig: validate?, mode?, reValidateMode? |
.setSubmitOptions(options) | Default submit options |
.addFieldValidation(fieldId, config) | Attach validation after creation |
.addFieldConditions(fieldId, conditions) | Attach ConditionalBehavior (visible?, disabled?, required?, readonly?) |
.updateField(fieldId, updates) / .removeField(fieldId) | Mutate fields |
.getField(id) / .getFields() / .getRows() | Read back |
.setId(id) / .clear() / .clone(newId?) | Utilities |
.validate() | Structural issues as string[] |
.build() | FormConfiguration<C>; throws on invalid |
.toJSON() / .fromJSON(json) / .getStats() | Serialization, stats |
Validation timing — form-level, two-phase (mirrors React Hook Form):
mode:'onSubmit' | 'onBlur' | 'onChange' | 'onTouched' | 'all'— when a field first validates. Default'onTouched'(first blur, then live). RHF's own default isonSubmit; RilayKit keeps its historical behavior.reValidateMode:'onChange' | 'onBlur' | 'onSubmit'— re-validation once a field has errored. Default'onChange'(errors clear live).- Per field:
validation: { validate, debounceMs }.debounceMsdefers change-triggered async validation only; blur and submit always validate immediately. - Submit always validates and marks errored fields
touched, so touched-gated renderers show submit errors, and they clear live.
Error routing — cross-field issues from setValidation's validate are written to the store by issue.path: a path naming a known field attaches to that field (tagged FORM_LEVEL_ERROR_CODE); empty or unmatched paths land in the __form__ bucket read by useFormErrors(). Form-level rules re-run on the same mode/reValidateMode cadence and on repeatable row add/remove/move.
Hooks
All from @rilaykit/forms/react, Zustand-backed: field-level hooks re-render only when their field changes (a keystroke re-renders only the typed field).
| Field hook | Returns |
|---|---|
useFieldValue<T>(fieldId) | T |
useFieldErrors(fieldId) | FieldError[] (includes routed cross-field issues) |
useFieldTouched(fieldId) | boolean |
useFieldValidationState(fieldId) | 'idle' | 'validating' | 'valid' | 'invalid' |
useFieldConditions(fieldId) | { visible, disabled, required, readonly } |
useFieldState(fieldId) | { value, errors, validationState, touched, dirty } |
useFieldProps(fieldId) | Record<string, unknown> |
useFieldActions(fieldId) | { setValue, setTouched, setErrors, clearErrors, setValidationState } |
| Form hook | Returns |
|---|---|
useFormErrors() | FieldError[] — the __form__ bucket; use for a form-level error banner (aria-live) |
useFormValues() | Record<string, unknown> |
useFormSubmitting() / useFormValid() / useFormDirty() | boolean (isValid counts the whole map, __form__ included) |
useFormSubmitState() | { isSubmitting, isValid, isDirty } |
useFormActions() | { setValue, setTouched, setErrors, setSubmitting, reset, setFieldConditions } |
useRepeatableKeys(repeatableId) | string[] — live row keys |
useFormStoreApi() | Store ref for imperative getState() reads |
useForm() | Form context (config, validation, submit) |
reset(values?, repeatableOrder?) restores defaults or explicit values; the second argument restores repeatable row order.
Server-Driven Forms (JSON schemas)
import { compileForm, fromSchema, validateSchema, isFormSchema } from '@rilaykit/forms';
const { formConfig, defaultValues } = compileForm(schema, catalog, { bindings });| Export | Description |
|---|---|
compileForm(schema, catalog, options?) | Compile a FormSchema → { formConfig, defaultValues? }. options.bindings resolves custom validator/effect string references; options can also enable per-field props validation |
fromSchema(schema, catalog, registry?) | Same result, registry-style signature |
validateSchema(schema, catalog, registry?) | Throws SchemaValidationError (with pathed issues[]) on invalid structure |
isFormSchema(value) | Type guard |
interface FormSchema {
readonly version?: 1;
readonly id: string;
readonly defaultValues?: Record<string, unknown>;
readonly fields?: FormSchemaField[]; // flat layout
readonly rows?: FormSchemaRow[]; // row-based layout (incl. repeatables)
readonly validation?: FormSchemaValidationConfig; // { rules?, mode?, reValidateMode? }
readonly submitOptions?: SubmitOptions;
}
interface FormSchemaField {
readonly id: string;
readonly type: string;
readonly props?: Record<string, unknown>;
readonly validation?: { rules?: ValidationDescriptor | ValidationDescriptor[]; debounceMs?: number };
readonly conditions?: ConditionalBehavior;
readonly effects?: FieldSchemaEffect[];
}
// rules: 'required' | 'email' | 'url' | 'number' (shortcuts)
// or { type: string; message?: string; params?: Record<string, unknown> }
interface SchemaRegistry { // alias: Bindings
readonly validators?: Record<string, CustomValidatorFactory>;
readonly effects?: Record<string, SchemaEffectHandler>;
}@rilaykit/workflow
Builder and flow-schema compiler from @rilaykit/workflow; components and hooks from @rilaykit/workflow/react.
Components
import { Flow } from '@rilaykit/workflow/react';
<Flow of={onboarding} defaults={{ name: '' }} onComplete={(data, meta) => save(data, meta)}>
<Flow.Progress />
<Flow.Body />
<Flow.Back />
<Flow.Skip />
<Flow.Next />
</Flow>Flow
Compound root (Flow.Body, Flow.Progress, Flow.Next, Flow.Back, Flow.Skip). Wraps WorkflowProvider.
| Prop | Type | Description |
|---|---|---|
of | WorkflowConfig | flow | Built configuration or builder (auto-built) |
defaults? | Record<string, unknown> | Initial data |
defaultStep? | string | Step ID to start on |
onStepChange? | (from: number, to: number, context) => void | Step transition callback |
onComplete? | (data, meta: WorkflowCompletionMeta) => void | Promise<void> | See below |
className? | string | CSS class |
Completion: data is a pure projection — only answered visible steps; a skipped or never-visible step is absent (no {} placeholder). meta ({ visitedSteps, skippedSteps, passedSteps }, string[] in insertion order, exported from @rilaykit/workflow/react) is additive: onComplete(data) callers keep working. visitedSteps records steps navigated to; the entry step is not included.
| Component | Props |
|---|---|
Flow.Body | stepId? (render only for that step), children?: ReactNode | (({ step }) => ReactNode) — falls back to the step's renderer, then <FormBody /> |
Flow.Progress | children?: (ctx: FlowStepsContext) => ReactNode ({ steps, currentIndex, goTo }), className? |
Flow.Next / Flow.Back / Flow.Skip | children?: ReactNode | ((ctx: FlowNavContext) => ReactNode) ({ go, canGo, submitting, isLastStep, step }), className? |
useFlow() returns the full WorkflowContextValue: workflow state, currentStep, navigation (goToStep, goNext, goPrevious, skipStep, canGoNext, …), data actions (setValue, setStepData, resetWorkflow), submission (submitWorkflow, canSubmit), and persistence (persistNow, isPersisting, persistenceError).
flow (FlowBuilder)
import { flow } from '@rilaykit/workflow';
const onboarding = flow.create(catalog, 'onboarding', 'User Onboarding')
.step({
id: 'basics',
title: 'Basics',
formConfig: basicsForm,
after: async (step) => {
const company = await fetchCompany(step.data.siren);
step.next.prefill({ companyName: company.name });
},
})
.step([
{ title: 'Company', formConfig: companyForm },
{ title: 'Review', formConfig: reviewForm, allowSkip: true },
]);StepDefinition: id?, title, description?, formConfig (built config or builder), allowSkip? (boolean or predicate on workflow data), renderer?, conditions? ({ visible?, skippable? }), metadata?, after?.
after(step) runs after successful validation, before navigating. StepContext:
| Member | Description |
|---|---|
step.data | The step's validated data |
step.next.prefill(fields) | Write fields into the next visible step (a conditionally hidden step never swallows the prefill). Overwrite-always and re-runs on every forward transition — guard on the value for seed-if-empty |
step.workflow.get(stepId) / step.workflow.all() | Other steps' data |
step.meta, step.isFirst, step.isLast | Metadata and position |
The legacy onAfterValidation(stepData, helper, context) signature still works but is deprecated — use after.
| Method | Description |
|---|---|
.step(def) / .step([defs]) (alias .addStep) | Add steps |
.configure({ analytics?, persistence? }) | analytics: WorkflowAnalytics; persistence: { adapter, options?, userId? } |
.use(plugin) / .removePlugin(name) | Plugins (dependencies validated on install) |
.updateStep(id, updates) / .removeStep(id) | Mutate steps |
.addStepConditions(id, { visible?, skippable? }) | Conditions after creation |
.getStep(id) / .getSteps() / .clearSteps() | Read back / reset |
.clone(newId?, newName?) / .validate() / .build() | Utilities; .build() throws on invalid |
.toJSON() / .fromJSON(json) / .getStats() | Serialization, stats |
Skip semantics: a step is re-skippable after navigating back onto it; a step later completed stops counting as skipped. onStepSkip(stepId, reason) currently always reports 'user_skip'.
Hooks
From @rilaykit/workflow/react.
| Hook | Returns |
|---|---|
useFlow() | Full workflow context (see above) |
useFlowStepIndex() | number |
useFlowTransitioning() / useFlowInitializing() / useFlowSubmitting() | boolean |
useFlowData() | All data across steps |
useStepData() / useStepDataById(stepId) | Current / specific step data |
useVisitedSteps() / usePassedSteps() / useSkippedSteps() | Set<string> |
useIsStepVisited(id) / useIsStepPassed(id) / useIsStepSkipped(id) | boolean |
useFlowNavigationState() | { currentStepIndex, isTransitioning, isSubmitting } |
useFlowSubmitState() | { isSubmitting, isTransitioning, isInitializing } |
useFlowActions() | Store actions: setCurrentStep, setStepData, setAllData, setFieldValue, setSubmitting, setTransitioning, setInitializing, markStepVisited/Passed/Skipped, reset, loadPersistedState |
useFlowSteps() | { steps, currentIndex, goTo } (drives Flow.Progress) |
useStepMetadata() | current, getByStepId, getByStepIndex, hasCurrentKey, getCurrentValue, getAllStepsMetadata, findStepsByMetadata |
useFlowStoreApi() | Store ref for imperative reads |
Persistence
Configured via .configure({ persistence: { adapter, options?, userId? } }).
- Values survive save→load byte-faithfully:
Date,NaN,±Infinity,-0,BigIntare tag-encoded; legacy plain-JSON blobs still load. - A pending debounced autosave is flushed on unmount; completion clears persisted data and a late in-flight save cannot resurrect it.
- Corrupted blobs degrade to a fresh start (
LOAD_FAILEDonpersistenceError); out-of-range step indexes clamp; resume into a now-hidden step relocates forward to the next visible step.
Analytics
Every workflow error path routes through analytics.onError(error, context) and the monitoring adapter: step-transition failures, after throws, submission throws, and persistence save/load/remove failures (WorkflowPersistenceError). A validation error blocking Next is not an error path. The last step fires no onStepComplete — completion is carried by onWorkflowComplete(id, duration, data) with the same projected data as onComplete.
Flow Schemas
import { compileFlow, validateFlowSchema, isFlowSchema } from '@rilaykit/workflow';
const { workflowConfig } = compileFlow(schema, catalog, { bindings });validateFlowSchema reports pathed issues; SchemaValidationError is re-exported from @rilaykit/workflow for flow-schema failures.
@rilaykit/agent
AI-agent integration (re-exported by rilaykit). Mains are React-free. See the AI guide for the full walkthrough.
| Export | Entry | Description |
|---|---|---|
manifest(catalog) | main | Markdown catalog manifest for the system prompt |
uiTools() | main | Registers show_form / show_flow / show_component (schema-only tools, no execute) |
parsePartialJson(text) | main | Streaming-tolerant JSON parsing |
Part, isTextPart, isToolPart, isDataPart | main | Normalized message-part model |
Catalog, Parts, Part, ShowForm, ShowFlow, ShowComponent | /react | HITL rendering components |
tools(catalog), toParts(message) | rilaykit/ai-sdk | Vercel AI SDK adapter — tools() is assignable to ToolSet with no cast; toParts maps UIMessage parts (all four tool states, dynamic-tool, data-*) |
tools(catalog), toParts(message) | rilaykit/anthropic | Anthropic SDK adapter — Anthropic.Tool[]-compatible, maps text / tool_use blocks |
Tool JSON schemas come from the Standard Schema's ~standard.jsonSchema.output or a manual inputJsonSchema; a tool with neither is dropped and logged. Hosts must register a .part('text', …) renderer — there is no default. show_form resolves { status: 'submitted', values } or { status: 'cancelled' } exactly once per toolCallId.
Types
Validation
interface FieldError {
readonly message: string;
readonly code?: string;
readonly path?: string;
}
interface ValidationResult {
readonly isValid: boolean;
readonly errors: FieldError[];
readonly value?: any;
}
type ValidationState = 'idle' | 'validating' | 'valid' | 'invalid';
interface FieldValidationConfig<T = any> {
readonly validate?: StandardSchema<T> | StandardSchema<T>[];
readonly debounceMs?: number; // async cost control; blur/submit validate immediately
}
type FormValidationMode = 'onSubmit' | 'onBlur' | 'onChange' | 'onTouched' | 'all';
type FormReValidateMode = 'onChange' | 'onBlur' | 'onSubmit';
interface FormValidationConfig<T extends Record<string, any> = Record<string, any>> {
readonly validate?: StandardSchema<T> | StandardSchema<T>[];
readonly mode?: FormValidationMode; // default 'onTouched'
readonly reValidateMode?: FormReValidateMode; // default 'onChange'
readonly validateOnStepChange?: boolean;
}Conditions
interface ConditionConfig {
field: string;
operator: ConditionOperator;
value?: ConditionValue;
conditions?: ConditionConfig[];
logicalOperator?: 'and' | 'or';
}
type ConditionValue =
| string | number | boolean | null | undefined
| RegExp | Array<string | number | boolean>;
interface ConditionalBehavior {
readonly visible?: ConditionConfig;
readonly disabled?: ConditionConfig;
readonly required?: ConditionConfig;
readonly readonly?: ConditionConfig;
}
interface StepConditionalBehavior {
readonly visible?: ConditionConfig;
readonly skippable?: ConditionConfig;
}Workflow
interface WorkflowCompletionMeta {
readonly visitedSteps: string[];
readonly skippedSteps: string[];
readonly passedSteps: string[];
}
interface WorkflowAnalytics {
readonly onWorkflowStart?: (workflowId: string, context: WorkflowContext) => void;
readonly onWorkflowComplete?: (workflowId: string, duration: number, data: any) => void;
readonly onWorkflowAbandon?: (workflowId: string, currentStep: string, data: any) => void;
readonly onStepStart?: (stepId: string, timestamp: number, context: WorkflowContext) => void;
readonly onStepComplete?: (stepId: string, duration: number, data: any, context: WorkflowContext) => void;
readonly onStepSkip?: (stepId: string, reason: string, context: WorkflowContext) => void;
readonly onError?: (error: Error, context: WorkflowContext) => void;
}
interface WorkflowPlugin {
readonly name: string;
readonly version?: string;
readonly install: (workflow: any) => void;
readonly dependencies?: string[];
}For MonitoringConfig and the monitoring pipeline, see the monitoring docs.