Skip to content

Custom Catalogs

This guide explains how to create a custom Freesail catalog — a package that bundles a JSON schema describing UI components with their concrete React implementations. Agents use the schema to know what components exist; the React code renders them in the browser.

Terminal window
npx freesail new catalog

This scaffolds a complete catalog package using the inclusion model. You own every file and can modify them freely.

{name}-catalog/
package.json
tsconfig.json
src/
{name}-catalog.json # Generated — full resolved catalog (do not edit directly)
freesailconfig.json # Catalog metadata (catalogId, title, description)
index.ts # Exports CatalogDefinition
includes/
catalog.include.json # Declare which packages to import from
generated-includes.ts # Auto-generated bridge (do not edit)
components/
components.json # Custom component schemas
components.tsx # Custom component implementations
functions/
functions.json # Custom function schemas
functions.ts # Custom function implementations

The catalogId in the generated catalog JSON is derived from the npm package name’s org scope. During scaffolding, you can provide your own org name, or accept the generated placeholder (e.g. @catamaran-4f8a2c). The resulting catalogId will look like https://acme.local/catalogs/weather-catalog.json.

To override the derived catalogId, edit src/freesailconfig.json:

{
"catalog": {
"catalogFile": "weather-catalog.json",
"catalogId": "https://mycompany.com/catalogs/weather-catalog.json",
"title": "Weather Catalog",
"description": "Weather UI components"
}
}

The generated package.json includes:

{
"scripts": {
"prepare:catalog": "freesail prepare catalog",
"prebuild": "freesail prepare catalog && freesail validate catalog",
"build": "tsc"
}
}
  • freesail prepare catalog — reads catalog.include.json, merges imported and local schemas, and writes {name}-catalog.json and generated-includes.ts
  • freesail validate catalog — checks that every JSON-declared component/function has a matching implementation

Both run automatically before each npm run build.


The inclusion model lets you pull components and functions from any installed catalog package into your own. This is the primary way to reuse the standard Freesail components.

Terminal window
npx freesail include catalog --package @freesail/standard-catalog

This command:

  1. Reads all components and functions from the installed package’s catalog JSON
  2. Writes them into src/includes/catalog.include.json
  3. Re-runs freesail prepare catalog

Edit catalog.include.json afterwards to keep only what you need:

{
"includes": {
"@freesail/standard-catalog": {
"catalogPath": "dist/standard-catalog.json",
"components": ["Card", "Button", "Text", "Column", "Row"],
"functions": ["formatString", "required", "formatDate"]
}
}
}

You can import from multiple packages by running freesail include catalog once per package, or by editing catalog.include.json directly.


Add custom component schemas in src/components/components.json:

{
"components": {
"StatusCard": {
"type": "object",
"allOf": [
{ "$ref": "#/$defs/ComponentCommon" },
{ "$ref": "#/$defs/CatalogComponentCommon" },
{
"type": "object",
"description": "A card displaying a status with a title, message, and severity level.",
"properties": {
"component": { "const": "StatusCard" },
"title": { "$ref": "#/$defs/DynamicString", "description": "Card heading" },
"message": { "$ref": "#/$defs/DynamicString", "description": "Body text" },
"severity": {
"type": "string",
"enum": ["info", "warning", "error", "success"],
"description": "Visual severity level"
}
},
"required": ["component", "title"]
}
],
"unevaluatedProperties": false
}
}
}

Add custom function schemas in src/functions/functions.json:

{
"functions": {
"truncate": {
"type": "object",
"description": "Truncates a string to maxLength characters.",
"properties": {
"call": { "const": "truncate" },
"args": {
"type": "object",
"properties": {
"value": { "$ref": "#/$defs/DynamicValue" },
"maxLength": { "type": "number" }
},
"required": ["value"]
},
"returnType": { "const": "string" }
},
"required": ["call", "args"],
"unevaluatedProperties": false
}
}
}

After editing, run npx freesail prepare catalog to regenerate the resolved catalog JSON.

Key rules:

  • components keys are the component names agents will use (e.g. "component": "StatusCard").
  • description fields are included in the agent’s system prompt — write them clearly.
  • Use allOf with $ref: "#/$defs/ComponentCommon" for consistent component structure.
  • Do not edit {name}-catalog.json directly — it is regenerated by freesail prepare catalog.

Step 2: Implement Components (components/components.tsx)

Section titled “Step 2: Implement Components (components/components.tsx)”

The scaffolded file imports included components from generated-includes.ts. Add your custom components alongside:

import React, { type CSSProperties } from 'react';
import type { FreesailComponentProps } from '@freesail/react';
import { includedComponents } from '../includes/generated-includes.js';
export function StatusCard({ component, children }: FreesailComponentProps) {
const title = (component['title'] as string) ?? '';
const message = (component['message'] as string) ?? '';
const severity = (component['severity'] as string) ?? 'info';
const colors: Record<string, string> = {
info: 'var(--freesail-info, #3b82f6)',
warning: 'var(--freesail-warning, #f59e0b)',
error: 'var(--freesail-error, #ef4444)',
success: 'var(--freesail-success, #22c55e)',
};
const style: CSSProperties = {
padding: '16px',
borderRadius: '8px',
border: `1px solid ${colors[severity] ?? colors['info']}`,
};
return (
<div style={style}>
<strong>{title}</strong>
{message && <p style={{ margin: '8px 0 0' }}>{message}</p>}
{children}
</div>
);
}
export const myappCatalogComponents = {
...includedComponents,
StatusCard,
};

Conventions:

  • Export each component as a named export function.
  • Cast props with as string — all values arrive as unknown.
  • Use CSS custom properties (var(--freesail-*)) for theming.
  • The map keys must exactly match the component names in the JSON schema.
PropTypePurpose
componentA2UIComponentAll resolved props the agent sent for this component instance
childrenReactNodeRendered child components (for containers)
metaComponentMetaMetadata object with getBinding(propName) for accessing raw data bindings
scopeDataunknownCurrent item data when inside a dynamic list template
dataModelRecord<string, unknown>Full surface data model (read-only snapshot)
onAction(name, context) => voidDispatch a named action to the agent
onDataChange(path, value) => voidWrite a value to the local data model (two-way binding)
onFunctionCall(call) => voidExecute a client-side function call

For components that let users enter data, use the meta object to resolve the bound data model path, then call onDataChange on every change:

export function MyInput({ component, meta, onDataChange }: FreesailComponentProps) {
const value = (component['value'] as string) ?? '';
const boundPath = meta.getBinding('value')?.path ?? `/input/${component.id}`;
return (
<input
value={value}
onChange={(e) => onDataChange?.(boundPath, e.target.value)}
/>
);
}

Step 3: Add Custom Functions (functions/functions.ts)

Section titled “Step 3: Add Custom Functions (functions/functions.ts)”

The scaffolded file re-exports included functions. Add custom functions alongside:

import type { FunctionImplementation } from '@freesail/react';
import { includedFunctions } from '../includes/generated-includes.js';
const truncate: FunctionImplementation = (value: unknown, maxLength: unknown) => {
const str = String(value ?? '');
const max = Number(maxLength ?? 100);
return str.length > max ? str.slice(0, max) + '' : str;
};
export const myappCatalogFunctions = {
...includedFunctions,
truncate,
};

Remember to also declare the function in src/functions/functions.json (see Step 1).


The scaffolded index.ts is ready to use:

import type { CatalogDefinition } from '@freesail/react';
import { myappCatalogComponents } from './components/components.js';
import { myappCatalogFunctions } from './functions/functions.js';
import catalogSchema from './myapp-catalog.json';
export const MyappCatalog: CatalogDefinition = {
namespace: catalogSchema.catalogId,
schema: catalogSchema,
components: myappCatalogComponents,
functions: myappCatalogFunctions,
};

formatString is required. Both freesail prepare catalog and freesail validate catalog will warn if it is missing. Import it from @freesail/standard-catalog via catalog.include.json, or implement it yourself.


Pass your catalog to FreesailProvider via the catalogs prop:

import { ReactUI } from 'freesail';
import { MyappCatalog } from 'myapp-catalog';
function App() {
return (
<ReactUI.FreesailProvider
theme={ReactUI.defaultLightTokens}
catalogs={[MyappCatalog]}
>
{/* your app */}
</ReactUI.FreesailProvider>
);
}

Run validation manually at any time:

Terminal window
npx freesail prepare catalog # Merge schemas and regenerate catalog JSON
npx freesail validate catalog # Check implementations match the schema

Both accept --dir <path> to target a catalog in a different directory.

Validation checks:

  • Every component key in the catalog JSON has a matching entry in the components map.
  • Every function key in the catalog JSON has a matching implementation.
  • formatString is present in the catalog (warns if missing).
  • Required schema fields (catalogId, title) are set.
  • Warns if catalogId uses a .local placeholder domain.

PropertyTypeRequiredDescription
namespacestringThe catalogId URI — must match schema.catalogId
schemaobjectThe parsed JSON schema object
componentsRecord<string, ComponentType<FreesailComponentProps>>Component name → React component map
functionsRecord<string, FunctionImplementation>Function name → implementation map (must include formatString)