rilaykit

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.

On this page