Why RilayKit
Understand the schema-first approach to building forms and workflows in React, and why it matters for production applications.
The Problem
Forms in React are usually built imperatively: JSX mixed with state management, validation scattered across components, nothing serializable, and every multi-step flow a hand-rolled state machine. UI and business logic end up coupled, every form is a one-off, and type safety is bolted on.
RilayKit treats forms as data structures instead of UI trees.
Schema-First: Forms as Data
Form configurations are plain, serializable objects. You describe what a form contains; the library handles rendering and behavior.
import { ril, required, email } from '@rilaykit/core';
import { form } from '@rilaykit/forms';
const rilay = ril.create()
.component('input', { renderer: YourInputComponent });
const onboardingForm = form.create(rilay, 'onboarding')
.add({
id: 'email',
type: 'input',
props: { label: 'Email' },
validation: { validate: [required(), email()] },
});
// A form config is just data -- serialize it, store it, version it
const json = onboardingForm.toJSON();Because a form is data, it is introspectable (iterate fields, read rules), serializable (store in a database, send over the network), clonable (form.clone() for A/B variants or per-tenant customization), and generatable from a server, CMS, or visual editor.
Component Registry: Define Once, Use Everywhere
Components are registered once on the ril instance; every form uses them. The same schema renders with completely different design systems:
// Design system A
const rilayMaterial = ril.create()
.component('input', { renderer: MaterialInput });
// Design system B
const rilayShadcn = ril.create()
.component('input', { renderer: ShadcnInput });
// Same form definition works with both
const loginForm = form.create(rilayMaterial, 'login')
.add({ id: 'email', type: 'input', props: { label: 'Email' } });Your UI components stay in your design system. RilayKit handles state, validation, conditions, and rendering orchestration -- no imposed styling, no vendor lock-in on the visual layer.
Type Propagation: Safety Without Boilerplate
.component('input', { renderer: MyInput }) captures MyInput's exact props interface. Every later .add({ type: 'input', props }) gets autocompletion and compile-time checking. You never write type annotations for forms -- types flow from component definitions through the builder chain.
const rilay = ril.create()
.component('text', { renderer: TextInput }) // captures TextInputProps
.component('select', { renderer: SelectInput }); // captures SelectProps
form.create(rilay, 'example')
.add({
id: 'country',
type: 'select', // 'type' narrowed to 'text' | 'select'
props: {
label: 'Country',
options: [{ value: 'us', label: 'US' }], // required by SelectProps
},
});Universal Validation
A single validation.validate field accepts built-in validators, custom functions, and any Standard Schema library (Zod, Valibot, ArkType, ...) -- mixed in the same array, no adapters:
import { z } from 'zod';
import { required, custom } from '@rilaykit/core';
.add({
id: 'email',
type: 'input',
props: { label: 'Email' },
validation: {
validate: [
required('Email is required'),
z.string().email('Invalid format'),
custom((value) => !value.endsWith('.test'), 'No test domains'),
],
},
})Validation timing is configured once at the form level with .setValidation({ mode, reValidateMode }) -- the same two-phase model as React Hook Form (mode for first validation, reValidateMode once a field has errored).
Declarative Conditions
Conditional logic is declared with when() -- no useEffect, no manual subscriptions:
import { when } from '@rilaykit/core';
.add({
id: 'companyName',
type: 'input',
props: { label: 'Company Name' },
conditions: {
visible: when('accountType').equals('business'),
required: when('accountType').equals('business'),
},
})Conditions drive visibility, required, disabled, and readonly state. They compose with .and() / .or(), support dot-notation paths, and offer operators from equals through greaterThanOrEqual, contains, in, matches, and exists. A field hidden by a condition skips validation automatically.
Real Workflow Engine
@rilaykit/workflow is a workflow engine on the same schema-first foundation, not a wizard with hidden divs:
import { flow, LocalStorageAdapter } from '@rilaykit/workflow';
const onboarding = flow.create(rilay, 'onboarding', 'User Onboarding')
.step({ id: 'account', title: 'Create Account', formConfig: accountForm })
.step({ id: 'profile', title: 'Your Profile', formConfig: profileForm, allowSkip: true })
.configure({
persistence: {
adapter: new LocalStorageAdapter({ maxAge: 7 * 24 * 60 * 60 * 1000 }),
options: { autoPersist: true, debounceMs: 500 },
},
analytics: {
onStepComplete: (stepId, duration) => trackEvent('step_complete', { stepId, duration }),
onWorkflowComplete: (id, duration) => trackEvent('workflow_complete', { id, duration }),
},
});The engine provides step navigation with guards and step-level validation, persistence through an adapter interface (localStorage or any backend), analytics callbacks, a plugin system (.use(plugin)), and cross-step conditions via when('stepId.fieldId'). On completion, onComplete(data, meta) receives only the answered visible steps, plus meta.visitedSteps / skippedSteps / passedSteps.
Agent-Ready
Because forms and flows are data, @rilaykit/agent can hand them to an LLM: manifest(catalog) documents your catalog in the system prompt, uiTools() exposes show_form / show_flow / show_component as schema-only tools, and adapters for the Vercel AI SDK (rilaykit/ai-sdk) and Anthropic (rilaykit/anthropic) plug into real chat loops. The model asks; the user answers through your validated, type-safe form.
Who Is RilayKit For?
Teams building complex user-facing flows (SaaS onboarding, KYC, insurance claims, multi-step checkout), products with multiple design systems that share form logic, and developers who want multi-step workflows without hand-rolling state machines. MIT licensed, free, open source.
Head to the installation guide to set up RilayKit, or jump to the quickstart to build your first form.
What's New
RilayKit P3 — the agent package, two-phase validation timing, path-keyed errors, projected completion payloads, and hardening across persistence, observability, and rendering.
Quick Start
Get up and running with RilayKit in 5 minutes. Perfect for developers who want to jump straight into code.