rilaykit

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> });
MethodReturnsDescription
ril.create()ril<C>New empty catalog
.component(type, entry)new instanceRegister a component (entry: name?, description?, propsSchema?, propsJsonSchema?, renderer?, defaultProps?, validation?, meta?, replace?)
.tool(name, entry)new instanceRegister a tool (description?, inputSchema?, inputJsonSchema?, renderer?, meta?, replace?)
.part(type, entry)new instanceRegister a message-part renderer (renderer required)
.use(plugin)new instanceApply a RilayPlugin ((r) => r.component(...)...)
.renderers({ components?, tools?, parts? })new instanceAttach renderers to already-registered entries
.getComponent(id) / .getTool(name) / .getPart(type)entry or undefinedLookup
.getAllComponents() / .getAllTools() / .getAllParts()arraysEnumerate
.hasComponent(id)booleanExistence check
.validateProps(type, props)PropsValidationResultCheck props against the component's propsSchema
.validate() / .validateAsync()string[] / Promise<AsyncValidationResult>Configuration validation
.getStats(){ total, components, tools, parts }Counts
.clone() / .removeComponent(id) / .clear()new instanceImmutability 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';
ValidatorSignature
requiredrequired(msg?)
emailemail(msg?)
urlurl(msg?)
minLength / maxLengthminLength(min, msg?) / maxLength(max, msg?)
patternpattern(regex, msg?)
numbernumber(msg?)
min / maxmin(val, msg?) / max(val, msg?)
customcustom<T>(fn, msg?) — sync (value: T) => boolean
asyncasync<T>(fn, msg?)(value: T) => Promise<boolean>
combinecombine<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 work

Operators: .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 methodDescription
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.

PropTypeDescription
ofFormConfiguration | formBuilt 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) => voidPer-change callback
onFieldsRemove?(fieldIds, formData) => voidFired when repeatable-row removal deletes keys
onRepeatableOrderChange?(order) => voidLive row-order mirror
defaultRepeatableOrder?Record<string, string[]>Row order to restore
instanceId?stringOwner identity; same config + different instanceId = different form
conditionValues?Record<string, unknown>Read-only extra values conditions may reference (cross-step)
className?stringCSS class

Form.Body / Form.Field / Form.Submit / Form.List

ComponentProps
Form.Bodychildren?: ({ rows }) => ReactNode, className? — renders all visible rows by default
Form.Fieldid, config?, disabled?, overrides? (extra props merged in), forceVisible?, className?
Form.Submitchildren?: ReactNode | (({ submitting, submit }) => ReactNode), className?
Form.Listid, 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
      }),
  });
MethodDescription
.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 is onSubmit; 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 }. debounceMs defers 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 hookReturns
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 hookReturns
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 });
ExportDescription
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.

PropTypeDescription
ofWorkflowConfig | flowBuilt configuration or builder (auto-built)
defaults?Record<string, unknown>Initial data
defaultStep?stringStep ID to start on
onStepChange?(from: number, to: number, context) => voidStep transition callback
onComplete?(data, meta: WorkflowCompletionMeta) => void | Promise<void>See below
className?stringCSS 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.

ComponentProps
Flow.BodystepId? (render only for that step), children?: ReactNode | (({ step }) => ReactNode) — falls back to the step's renderer, then <FormBody />
Flow.Progresschildren?: (ctx: FlowStepsContext) => ReactNode ({ steps, currentIndex, goTo }), className?
Flow.Next / Flow.Back / Flow.Skipchildren?: 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:

MemberDescription
step.dataThe 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.isLastMetadata and position

The legacy onAfterValidation(stepData, helper, context) signature still works but is deprecated — use after.

MethodDescription
.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.

HookReturns
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, BigInt are 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_FAILED on persistenceError); 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.

ExportEntryDescription
manifest(catalog)mainMarkdown catalog manifest for the system prompt
uiTools()mainRegisters show_form / show_flow / show_component (schema-only tools, no execute)
parsePartialJson(text)mainStreaming-tolerant JSON parsing
Part, isTextPart, isToolPart, isDataPartmainNormalized message-part model
Catalog, Parts, Part, ShowForm, ShowFlow, ShowComponent/reactHITL rendering components
tools(catalog), toParts(message)rilaykit/ai-sdkVercel 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/anthropicAnthropic 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.

On this page