Skip to main content

Schema Reference

This is the technical reference for JSON schemas that drive Raytio's dynamic form generation system (the "Wizard"). It lists every property, tag, and configuration option.

For a conceptual overview of how the schema system works, see Understanding the Schema System.


1. Schema Naming Rules

Schema names must follow this pattern:

^(p|u|s)s_[A-Za-z0-9_]+$
PrefixTypeDescription
ss_System SchemaManaged by Raytio, shared across tenants
ps_Profile SchemaUser-specific data schemas
us_User SchemaCustom user-defined schemas

Examples:

  • ss_Driver_License
  • ps_My_Custom_Form
  • us_Company_Onboarding
  • driver_license (missing prefix)
  • ss-driver-license (hyphens not allowed)

1.1 Reserved Field Names

These field names are banned and cannot be used:

Reserved NameReason
encrypted_dataInternal encryption
encrypted_keyInternal encryption
n_idProfile Object ID
validationVerification system
encryptEncryption flag
contentMedia content
content_typeMIME type
listing_typeLegacy permissions
permissionsAccess control
__signatureDigital signatures
verifyVerification system
field_listInternal use
breakdownInternal use
keyReserved
identity_document_picture_*Reserved pattern
videoReserved

Additional naming rules:

  • Cannot contain <=> (used for conditional field suffixes)
  • Cannot contain : (used in tag syntax)
  • Cannot end with _temp
  • Cannot start with __group_

2. Schema Structure

2.1 Basic Template

{
"title": "Person",
"title_plural": "People",
"description": "Basic person information",

"tags": ["action:verify"],

"properties": {
"given_name": {
"type": "string",
"title": "First Name",
"tags": ["group:name:person"],
"priority": 1
},
"family_name": {
"type": "string",
"title": "Last Name",
"tags": ["group:name:person"],
"priority": 2
}
},

"required": ["given_name", "family_name"],
"verified_fields": ["given_name", "family_name"],

"display": {
"head_main": { "fields": ["given_name", "family_name"] },
"head_sub": { "fields": ["email"] }
},

"i18n": {
"en": {
"$schema": { "title": "Person", "description": "..." }
}
}
}

2.2 Schema-Level Properties

PropertyTypeRequiredDescription
titlestringYesDisplay name of the schema
title_pluralstringNoPlural form (e.g., "People")
descriptionstringYesSchema description
propertiesobjectYesField definitions
requiredarrayNoRequired field names (see Validation)
verified_fieldsarrayNoFields requiring verification (see Verification)
tagsarrayNoSchema-level behavior tags
schema_groupstringNoGroups similar schemas (e.g., "passports")
search_termsstring[]NoAdditional search terms/aliases for discoverability (e.g., ["license", "drivers license"])
schema_type"ss"|"ps"|"us"NoSystem/Profile/User schema
schema_country_codesstring[]NoCountry availability (2-char ISO codes)
displayobjectNoDisplay configuration (see below)
relationshipsarrayNoRelated schema definitions (see Relationships)
i18nobjectNoLocalization overrides
suggest_post_createstringNoSuggested next schema after creation
onboard_propertiesobjectNoOnboarding wizard configuration (see Onboarding Schema Guide)
groupsobjectNoGroup display configuration (see Group Display Order)
databaseobjectNoDatabase configuration for admin dashboard tables (see Database Configuration)

2.3 Country-Restricted Schemas

Use schema_country_codes to restrict schemas to specific countries:

{
"schema_name": "ss_NZ_Driver_License",
"schema_country_codes": ["NZ"],
"schema": {
"title": "New Zealand Driver License"
}
}
ValueMeaning
["NZ"]Only available in New Zealand
["AU", "NZ"]Available in Australia and New Zealand
[] or not setAvailable globally

How it works:

  1. Client detects user's country (profile or browser locale)
  2. Only schemas matching the user's country are shown
  3. Empty or missing = available everywhere

2.4 Display Configuration

The display object controls how ProfileObjects appear in the UI (not the form):

{
"display": {
"head_main": {
"fields": ["given_name", "family_name"],
"format": "{given_name} {family_name}"
},
"head_sub": {
"fields": ["email"]
},
"expand": [
{
"label": "Contact Details",
"fields": ["phone", "address"]
},
{
"label": "Work Information",
"fields": ["company", "job_title"]
}
],
"compact_table": true,
"tabular": {
"fields": ["name", "email", "status"]
},
"filters": [
{
"name": "Active contacts",
"operation": "and",
"tokens": [
{ "propertyKey": "status", "operator": "=", "value": "active" }
]
}
]
}
}
PropertyTypeDescription
head_mainobjectPrimary header display
head_main.fieldsstring[]Fields to show in header
head_main.formatstringOptional format string with {field} placeholders
head_subobjectSecondary header (subtitle)
expandarrayCollapsible sections with labels
compact_tablebooleanUse compact table display
tabularobjectTable view configuration
filtersarrayPre-defined filter presets for admin tables (see below)
kanbanobjectKanban board configuration (see below)

Pre-defined Filter Presets

The filters array lets schema authors provide default filter presets that appear in the admin table's "Filters" dropdown under a Default filters group. Unlike user-saved filters, these cannot be deleted by users.

Each filter maps directly to a Cloudscape PropertyFilterQuery, so propertyKey values must match keys in the schema's properties.

{
"display": {
"tabular": {
"fields": ["id", "name", "active", "start_date", "end_date"]
},
"filters": [
{
"name": "Active applications",
"operation": "and",
"tokens": [
{ "propertyKey": "active", "operator": "=", "value": "true" }
]
},
{
"name": "Guest-list enabled",
"operation": "and",
"tokens": [
{
"propertyKey": "auth_list_enabled",
"operator": "=",
"value": "true"
},
{ "propertyKey": "active", "operator": "=", "value": "true" }
]
},
{
"name": "Expired",
"operation": "and",
"tokens": [
{ "propertyKey": "active", "operator": "=", "value": "false" }
]
}
]
}
}
PropertyTypeRequiredDescription
filters[].namestringYesDisplay name shown in the dropdown
filters[].operationstringYes"and" (all tokens must match) or "or" (any must match)
filters[].tokensarrayYesArray of filter conditions
filters[].tokens[].propertyKeystringYesMust match a key in schema properties
filters[].tokens[].operatorstringYesOne of: =, !=, :, !:, >, <, >=, <=
filters[].tokens[].valuestringYesValue to compare against

Operator reference:

OperatorMeaningExample
=Equalsactive = true
!=Not equalsstatus != archived
:Containsname : "bank"
!:Does not containname !: "test"
>Greater thanpriority > 5
<Less thanpriority < 10
>=Greater than or equalstart_date >= 2025-01-01
<=Less than or equalend_date <= 2025-12-31

How head_main is Used for Foreign Key Display

The head_main configuration is critical for foreign key ID-to-name resolution. When a field has a foreign_key_of property pointing to another schema, the client uses head_main from the foreign schema to determine what to display instead of the raw ID.

The resolution flow:

  1. User selects or views a foreign key field (e.g., party_id)
  2. Client looks up the referenced schema (e.g., ss_prm_party)
  3. Client finds the ProfileObject matching the stored ID
  4. useMainPOField hook extracts the head_main.fields from that ProfileObject
  5. If a format string exists, fields are joined using that pattern
  6. The formatted display value is shown instead of the raw ID

Example - Foreign schema configuration:

{
"name": "ss_prm_party",
"title": "Party",
"properties": {
"party_name": { "type": "string", "title": "Party Name" },
"party_type": { "type": "string", "title": "Party Type" }
},
"display": {
"head_main": {
"fields": ["party_name", "party_type"],
"format": "{party_name} ({party_type})"
}
}
}

Example - Referencing schema:

{
"name": "ss_contract",
"properties": {
"party_id": {
"type": "string",
"title": "Party",
"foreign_key_of": "ss_prm_party"
}
}
}

Result: Instead of displaying "party-abc-123", the UI shows "Acme Corp (Customer)".

Fallback behavior:

  • If no head_main is configured, the hook falls back to joining all available field values with spaces
  • If the foreign ProfileObject cannot be found, the raw ID is displayed
  • If lookup substitution is configured for a head_main field, the lookup value is used

Kanban Board Configuration

The kanban object configures how records are displayed in a kanban board view in the Admin Dashboard. This is used in conjunction with layout_type: "kanban" in layout_hierarchy.json.

{
"display": {
"kanban": {
"column_field": "status",
"title_field": "name",
"card_fields": ["description", "priority", "created_at"]
}
}
}
PropertyTypeRequiredDescription
column_fieldstringYesField that determines which column an item belongs to
title_fieldstringYesField to display as the card heading
card_fieldsstring[]NoAdditional fields to display on each card

How it works:

  1. The column_field value groups records into columns. If the field has an enum definition, columns are created for each enum value. Otherwise, columns are created for each unique value in the data.
  2. The title_field is displayed as the card heading (falls back to "Untitled" if empty).
  3. Each field in card_fields is displayed with its label and value on the card.
  4. Users can drag cards between columns to change the column_field value (persisted to the database).
  5. Users can drag columns to reorder them (order persists during the session).

Example schema with kanban configuration:

{
"name": "ss_fnd_job",
"title": "Background Job",
"properties": {
"name": { "type": "string", "title": "Job Name" },
"status": {
"type": "string",
"title": "Status",
"enum": ["pending", "running", "completed", "failed"]
},
"program_name": { "type": "string", "title": "Program" },
"scheduled_at": {
"type": "string",
"format": "date-time",
"title": "Scheduled"
}
},
"display": {
"kanban": {
"column_field": "status",
"title_field": "name",
"card_fields": ["program_name", "scheduled_at"]
}
}
}

Priority order for kanban configuration:

  1. currentView.kanban_config from available_layouts (for multi-view layouts)
  2. Schema's display.kanban property (recommended)
  3. layout_hierarchy.json kanban_config property (legacy/fallback)
  4. Auto-detection: looks for status/state/label fields for columns, title/name for card title

Note: To enable kanban view for a schema in the Admin Dashboard, you must also set layout_type: "kanban" in the corresponding entry in layout_hierarchy.json, or configure available_layouts for multi-view support. See the layout_hierarchy.json configuration for details.

2.5 Group Display Order

The groups object provides configuration options for field groups.

{
"title": "Party",
"properties": {
"name": {
"type": "string",
"tags": ["group:summary"],
"priority": 1
},
"email": {
"type": "string",
"tags": ["group:summary"],
"priority": 2
},
"internal_notes": {
"type": "string",
"tags": ["group:advanced"],
"priority": 1
}
},
"groups": {
"order": ["summary", "advanced"]
}
}

Groups Properties

PropertyTypeDescription
orderstring[]Array of group names specifying their display order

How Group Ordering Works

  1. Specified groups first: Groups listed in the order array appear first, in the exact order specified
  2. Unspecified groups after: Groups NOT in the order array appear after ordered groups, sorted by the priority of their first child field
  3. Backwards compatible: If groups.order is not specified, all groups sort by their first child's priority (original behavior)

Example:

{
"groups": {
"order": ["summary", "contact"]
}
}

With this configuration:

  • "summary" group appears first
  • "contact" group appears second
  • Any other groups (e.g., "advanced", "notes") appear after, sorted by priority

Special Groups

Group NameDescription
-notag-commonFields without any group tag (can be positioned)

Note: The -notag-common group collects fields that don't have a group:* tag. You can include it in the order array to position ungrouped fields.

2.6 Database Configuration

The database object configures how schemas interact with database tables in the Admin Dashboard. This is used for schemas that represent database records (as opposed to Profile Objects).

{
"database": {
"path": "/db/v1/rpc/my_function",
"primary_key": "id",
"select": "id,name,email,created_at",
"return_to": "ss_my_view_schema"
}
}

Database Properties

PropertyTypeRequiredDescription
pathstringNo*Complete API path (e.g., /db/v1/my_table or /db/v1/rpc/my_function)
tablestringNo*Table name, used to construct /db/v1/{table} when path is not specified
primary_keystringYesPrimary key column name (e.g., id)
selectstringNoPostgREST select parameter for specifying columns (e.g., id,name,email)
query_parametersstringNoPostgREST query parameters with variable support (see below)
return_tostringNoSchema name to redirect to after creating a record (for insert schemas)
method"POST" | "PATCH" | "DELETE"NoHTTP method override. Use POST for RPC endpoints (default varies by action)

* Either path or table must be specified, but not both. If path is specified, it is used directly; otherwise /db/v1/{table} is constructed.

Path vs Table

Use path when:

  • Calling PostgreSQL RPC functions (e.g., /db/v1/rpc/my_insert_function)
  • Using non-standard API paths
  • Need full control over the endpoint

Use table when:

  • Accessing standard PostgREST tables
  • The endpoint follows the /db/v1/{table} convention

Examples:

// Using path for RPC function
{
"database": {
"path": "/db/v1/rpc/dsm_access_application_users_i",
"primary_key": "id",
"return_to": "ss_dsm_access_application_user"
}
}

// Using table for standard table access
{
"database": {
"table": "dsm_access_application_users",
"primary_key": "id",
"select": "id,email,aa_id,active"
}
}

The return_to Property

Use return_to for "insert" schemas that create records via RPC functions. After successful creation, the Admin Dashboard redirects to the specified view schema showing the new record.

Use case: When you have separate schemas for viewing vs inserting records:

  • Insert schema (ss_dsm_access_application_user_i): Calls RPC function with parameters p_aa_id, p_email
  • View schema (ss_dsm_access_application_user): Displays the full record from a database view
// Insert schema
{
"schema_name": "ss_dsm_access_application_user_i",
"database": {
"path": "/db/v1/rpc/dsm_access_application_users_i",
"primary_key": "id",
"return_to": "ss_dsm_access_application_user"
},
"properties": {
"p_aa_id": {
"type": "string",
"foreign_key_of": "ss_dsm_access_application"
},
"p_email": { "type": "string", "title": "Email address" }
},
"required": ["p_aa_id", "p_email"]
}

After creating a record, the user is redirected from /admin/ss_dsm_access_application_user_i/add to /admin/ss_dsm_access_application_user/{new_id}.

Query Parameters

The query_parameters property allows specifying additional PostgREST query parameters that are included in all read requests. This is useful for:

  • Filtering data at the database level (e.g., active=eq.true)
  • Specifying complex select statements with nested resources
  • Adding organization or tenant scoping

Variable Substitution:

Query parameters support variable substitution using the {$context.X} syntax:

VariableDescription
{$context.organizationId}Current organization ID
{$context.userId}Current user ID
{$context.tenantId}Current tenant ID

Example:

{
"database": {
"table": "price_lists",
"primary_key": "id",
"query_parameters": "org_id=eq.{$context.organizationId}&active=eq.true&select=*,versions(*,lines(*))"
}
}

This generates URLs like:

/db/v1/price_lists?org_id=eq.org-123&active=eq.true&select=*,versions(*,lines(*))&limit=20&offset=0

Precedence:

  • If query_parameters contains select=, the separate select property is ignored
  • query_parameters values override auto-generated parameters (filters, sorting) with the same key
  • Pagination (limit, offset) is always appended unless overridden in query_parameters

3. Field Reference

3.1 Field Templates

The SchemaEditor provides these pre-built field templates. Use these as starting points:

Text Field

{
"field_name": {
"type": "string",
"title": "Field Label"
}
}

Date Field

{
"date_field": {
"type": "string",
"format": "date",
"title": "Select Date"
}
}

DateTime Field

{
"datetime_field": {
"type": "string",
"format": "date-time",
"title": "Select Date and Time"
}
}

Number Field

{
"number_field": {
"type": "number",
"title": "Amount",
"minimum": 0,
"maximum": 1000
}
}

Money Field

{
"price": {
"type": "string",
"title": "Price",
"tags": ["display:currency"]
}
}

Star Rating Field

{
"rating": {
"type": "string",
"title": "Rating",
"tags": ["display:stars"]
}
}

Select (Dropdown) Field

{
"status": {
"type": "string",
"title": "Status",
"enum": ["active", "pending", "inactive"]
}
}

Multi-Select Field

{
"categories": {
"type": "array",
"title": "Categories",
"enum": ["tech", "finance", "health", "education"]
}
}

Checkbox Field

{
"agree_terms": {
"type": "boolean",
"title": "I agree to the terms",
"content": "I have read and agree to the Terms of Service"
}
}

Multi-Checkbox Field

{
"preferences": {
"type": "array",
"title": "Preferences",
"items": {
"type": "boolean",
"properties": {}
},
"lookup": "https://api.rayt.io/lookups/preferences.json"
}
}

File Upload Field

{
"document": {
"type": "string",
"title": "Upload Document",
"contentEncoding": "base64",
"tags": ["action:client_upload"]
}
}

Content Block (Display-Only Guidance)

Content blocks display markdown-formatted text to guide users through forms. They are not form inputs and are excluded from form submission.

Plain markdown (no styling):

{
"intro_text": {
"type": "string",
"title": "Getting Started",
"description": "Fill out the fields below. **Required fields** are marked with an asterisk.",
"tags": ["display:content-block"],
"priority": 10
}
}

Info alert (blue):

{
"help_info": {
"type": "string",
"title": "Helpful Information",
"description": "This is an **info** block for general guidance.",
"tags": ["display:content-block", "display:info"],
"priority": 20
}
}

Warning alert (orange):

{
"caution_notice": {
"type": "string",
"title": "Please Note",
"description": "This is a **warning** block for important notices.",
"tags": ["display:content-block", "display:warning"],
"priority": 30
}
}

Error alert (red):

{
"critical_notice": {
"type": "string",
"title": "Critical Information",
"description": "This is an **error** block for critical information.",
"tags": ["display:content-block", "display:error"],
"priority": 40
}
}

Key points:

  • Use tags: ["display:content-block"] to create a content block (with type: "string" for JSON Schema compliance)
  • The description field contains the markdown content
  • Use priority to position the content block among other fields
  • Add display:info, display:warning, or display:error tags for styled variants
  • Content blocks can be placed within groups using the group:* tag
  • Content blocks are automatically excluded from form submission

3.2 Core Properties

PropertyTypeDefaultDescription
typestring-Data type: string, number, integer, boolean, array, object
titlestring-Field label
title_pluralstring-Plural form of title
descriptionstring-Help text below field
defaultany-Default value (see below for special cases)
exampleany-Example value (for documentation)
readOnlybooleanfalseHide from wizard form
encryptbooleanfalseEncrypt field value when storing
encrypt_requirebooleanfalseUser must enable encryption to save
tagsarray[]Field behavior tags
prioritynumber0Display order (lower = earlier)

The default Property

The default property sets the initial value for a field. Its behavior varies by field type:

Standard Fields (string, number, boolean)

{
"status": {
"type": "string",
"default": "active"
},
"quantity": {
"type": "number",
"default": 1
},
"subscribe": {
"type": "boolean",
"default": true
}
}

Date Fields (format: "date")

For date fields, default is a number representing days from today:

{
"start_date": {
"type": "string",
"format": "date",
"default": 0
},
"expiry_date": {
"type": "string",
"format": "date",
"default": 90
}
}
Default ValueResult
0Today's date
77 days from now
9090 days from now
-3030 days ago
Not setField remains empty

DateTime Fields (format: "date-time")

For datetime fields, default is a number representing seconds from now:

{
"scheduled_at": {
"type": "string",
"format": "date-time",
"default": 3600
},
"expires_at": {
"type": "string",
"format": "date-time",
"default": 604800
}
}
Default ValueResult
0Current date/time
36001 hour from now (60×60)
8640024 hours from now (60×60×24)
6048007 days from now (60×60×24×7)
Not setField remains empty

Birth Date Fields (with group:date_picker:birth tag)

Birth date fields automatically default to today's date components when the date picker is used. No default is needed.

Important: No Default = Empty Field

If no default is specified for date/datetime fields, the field remains empty.

Previously, date fields without a default would auto-populate with the current date, causing issues when those fields were sent to the backend unexpectedly.

// ❌ OLD BEHAVIOR (before fix): date_created would auto-fill with today
{
"date_created": {
"type": "string",
"format": "date-time",
"readOnly": true
}
}

// ✅ CURRENT BEHAVIOR: date_created stays empty unless default is set

// This is correct for server-generated timestamps

When to use default on date fields:

  • User-facing dates that should pre-fill (e.g., "Start Date" defaulting to today)
  • Expiry dates with a standard validity period

When NOT to use default:

  • Server-generated timestamps (date_created, date_updated)
  • Fields that should remain empty until user input
  • readOnly date fields populated by the backend

3.3 Type-Specific Properties

String Fields

PropertyTypeDescription
enumarrayAllowed values (≤7 = radio buttons, >7 = select)
patternstringRegex validation pattern
patternMessagestringCustom error for pattern failure
minLengthnumberMinimum string length
maxLengthnumberMaximum length (>30 = textarea)
formatstringSpecial format: date, date-time, uri
lookupstringURL to lookup JSON for dropdown

Number Fields

PropertyTypeDescription
minimumnumberMinimum value
maximumnumberMaximum value
stepnumberIncrement step
enumarraySpecific allowed values

Array Fields

PropertyTypeDescription
itemsobjectDefinition of array items
items.typestringType of items
items.propertiesobjectFor object arrays: nested fields
enumarrayFor multi-select from fixed options

Media/File Fields

PropertyTypeDescription
contentMediaTypestringMIME type (e.g., "image/jpeg")
contentEncoding"base64"Encoding specification
content_sourcestringImage source configuration
image_silhouettestringURL to camera overlay image

Table Fields (for array of objects)

PropertyTypeDescription
add_row_btn_labelstring"Add Row" button text
table_empty_messagestringMessage when table is empty

Reference Fields

PropertyTypeDescription
$refstringReference to sub-object schema
foreign_key_ofstringFK reference to another schema

The foreign_key_of Property

The foreign_key_of property creates a relationship between a field and another schema, enabling ID-to-name resolution in both forms and display views.

Basic usage:

{
"customer_id": {
"type": "string",
"title": "Customer",
"foreign_key_of": "ts_customer"
}
}

What happens in forms (editing):

  • The field renders as a searchable dropdown (ForeignKeySelect component)
  • The dropdown is populated with all ProfileObjects of the referenced schema
  • Each option's label is determined by the foreign schema's display.head_main configuration
  • The stored value is the ProfileObject's n_id (the actual ID)

What happens in display (viewing):

  • The field renders as a link (DisplayForeignKey component)
  • The link text shows the resolved name from head_main, not the raw ID
  • Clicking the link navigates to the referenced ProfileObject
  • If the foreign PO has a description property, it appears as a tooltip

Configuration requirements:

  1. The referencing field needs foreign_key_of pointing to the schema name
  2. The referenced schema should have display.head_main configured for meaningful display

Complete example:

// Referenced schema (ts_customer)
{
"name": "ts_customer",
"title": "Customer",
"properties": {
"customer_name": { "type": "string", "title": "Name" },
"customer_code": { "type": "string", "title": "Code" },
"description": { "type": "string", "title": "Description" }
},
"display": {
"head_main": {
"fields": ["customer_name", "customer_code"],
"format": "{customer_name} ({customer_code})"
}
}
}

// Referencing schema (ts_order)
{
"name": "ts_order",
"title": "Order",
"properties": {
"order_number": { "type": "string", "title": "Order Number" },
"customer_id": {
"type": "string",
"title": "Customer",
"foreign_key_of": "ts_customer"
}
},
"display": {
"head_main": { "fields": ["order_number"] }
}
}

Result:

  • Form shows a dropdown with options like "Acme Corp (ACME001)", "Beta Inc (BETA002)"
  • Stored value: "customer-uuid-123"
  • Display shows: "Acme Corp (ACME001)" as a clickable link

3.4 ReadOnly Behavior

The readOnly property controls field visibility in the Wizard:

Key rule: If ALL fields in a group have readOnly: true, the ENTIRE group is hidden.

Problem scenario:

{
"payment_id": { "readOnly": true, "tags": ["group:stripe_elements"] },
"card_last4": { "readOnly": true, "tags": ["group:stripe_elements"] },
"card_brand": { "readOnly": true, "tags": ["group:stripe_elements"] }
}

↑ ALL fields are readOnly = Stripe UI component is HIDDEN!

Solution: At least one field must NOT be readOnly:

{
"payment_id": { "tags": ["group:stripe_elements"] },
"card_last4": { "readOnly": true, "tags": ["group:stripe_elements"] },
"card_brand": { "readOnly": true, "tags": ["group:stripe_elements"] }
}

When to use readOnly: true:

Use CaseUse readOnly?
System-generated fields (IDs, timestamps)Yes
Fields populated by API extractionYes
Display-only fieldsYes
Fields in specialized DynamicSectionsAt least one must be false
Fields the user should editNo

4. Field Tags Reference

Field tags control rendering behavior. Format: "category:value" or "category:sub:value".

Group Tags (Determines Rendering Component)

TagTriggersDescription
group:addressAddressPickerDynamicSectionGoogle Places autocomplete
group:name:*PersonNameDynamicSectionCombined name picker
group:date_picker:*DatePickerDynamicSectionDate picker (day/month/year)
group:date_picker:birthDatePickerDynamicSectionBirth date with age validation
group:imagesImageDynamicSectionDocument capture + OCR
group:display_qr_codeQrCodeDynamicSectionQR code scanning flow
group:cameraCameraDynamicSectionPhoto capture
group:yodlee-accountsYodleeDynamicSectionYodlee banking
group:akahu-oauthAkahuDynamicSectionNZ banking (Akahu)
group:oauth2OAuthDynamicSectionOAuth 2.0 flow
group:stripe_elementsStripeDynamicSectionStripe payment
group:covidCovidDynamicSectionCOVID pass scanning
group:selectProfileObject:*SelectPODynamicSectionSelect existing PO
group:findProfileObject:*SelectPODynamicSectionLink to existing PO
group:autocompleteAutoCompleteDynamicSectionAutocomplete input
upload-group:*-File upload grouping

Date Component Tags

TagDescription
date_component:dayDay field in date picker group
date_component:monthMonth field in date picker group
date_component:yearYear field in date picker group

Display Tags

TagDescription
display:content-blockMarks field as a display-only content block
display:infoContent block variant: blue info alert
display:warningContent block variant: orange warning alert
display:errorContent block variant: red error alert
display:starsRender as star rating
display:no_autofillDisable browser autofill
display:currencyFormat as currency
display:cascadeCascading/hierarchical select
display:surveySurvey mode (rating buttons)
display:quotingQuote display mode
display:customModalCustom modal presentation
display:maskMask input (password-style)
display:replace:('old', 'new')String replacement display
display:terms_conditionsTerms checkbox styling
display:main_media:propNameMain media source property

Action Tags

TagDescription
action:allow_copyAdd clipboard copy button
action:allow_unreplaceAdd reveal button for masked fields
action:allow_password_compromise_checkCheck if password is breached
action:client_uploadClient-side file upload
action:hashCreate argon hash (use with hash_inputs)
action:require_webauthnRequire WebAuthN/MFA
action:generate_pepper:NGenerate N-byte random pepper
action:timeout:NAuto-refresh lookup after N seconds (see 11.7)
action:use_$ref:source.propertyPre-fill from selected PO (see below)

The action:use_$ref Tag

Pre-fill fields from a selected ProfileObject:

Format: action:use_$ref:[selectPO_field].[property]

{
"target_document": {
"type": "string",
"tags": ["group:selectProfileObject:ss_Identity"]
},
"first_name": {
"type": "string",
"tags": [
"group:name:person",
"action:use_$ref:target_document.first_given_name"
]
}
}

When user selects an identity document, first_name is auto-populated from the selected PO's first_given_name property.

Verification Tags

TagDescription
verify:show_if_pendingOnly show during pending verification

Type Tags

TagDescription
type:capture_geolocationCapture GPS coordinates
type:extract_requiredExtract from linked PO
type:extract_required:prop1,prop2Extract specific properties

5. Schema Tags Reference

Schema-level tags control form-wide behavior.

Action Tags

TagDescription
action:verifyEnable verification (requires verified_fields)
action:experimental_pass_object_store_idPass object store ID to API
action:display_qr_code:extract:startQR code extraction at start
action:display_qr_code:verify:pendingQR code for pending verification
action:display_global_idv_app_qr_code:extractGlobal IDV app extraction
action:reverify:display_qr_codeEnable QR code reverification flow

Camera Tags

TagDescription
default_camera:rearDefault to rear camera
default_camera:frontDefault to front camera

OAuth Tags

TagDescription
oauth2_component:name:ProviderNameOAuth provider display name
oauth2_component:redirect_url:URLOAuth redirect URL

Timing Tags

TagDescription
time:extract:30Estimated extraction time (seconds)
time:verify_pending_delay:60Verification delay (seconds)
time:live_person:45Live person verification time
TagDescription
link_to:ss_Schema:relationship_typeDefine schema relationships

Type Tags

TagDescription
type:service_providerService provider schema
type:service_offerService offer schema
type:marketplaceMarketplace schema
type:client_onlyClient-side only (not stored)
type:globally_unique_fieldContains globally unique fields
type:application_objectApplication object (skips verification)

Display Tags

TagDescription
display:default_view:editLink to edit mode from profile

6. Relationships

Relationships define connections between schemas. Configure in the relationships array:

{
"relationships": [
{
"relationship_name": "identity_documents",
"type": "owns",
"direction": "from",
"oneOf": ["ss_Passport", "ss_Driver_License"],
"multiple": true,
"required_relationship": false
},
{
"relationship_name": "employer",
"type": "works_at",
"direction": "from",
"anyOf": ["ss_Company", "instance"],
"multiple": false,
"required_relationship": true
}
]
}

Relationship Properties

PropertyTypeDescription
relationship_namestringUnique identifier for the relationship
typestringRelationship classification (e.g., "owns", "works_at")
direction"from"Always "from" (direction of relationship)
oneOfstring[]User must select ONE of these schemas
anyOfstring[]User can select ANY of these schemas
multiplebooleanAllow multiple relationships (default: true)
required_relationshipbooleanMust complete before sharing
propertiesobjectCustom fields on the relationship
requiredstring[]Required relationship fields

Special Value: "instance"

Use "instance" to allow linking to ANY ProfileObject:

{
"anyOf": ["ss_Company", "instance"]
}

This allows the user to either select a Company schema OR any other PO they have.


7. Dynamic Sections

Dynamic Sections are specialized React components that render groups of fields. The system matches fields by their group:* tags.

How Matching Works

  1. Fields are grouped by their group:* tag prefix
  2. Each DynamicSection has a GroupRule with a priority value
  3. Higher priority wins (most specific match)
  4. Default priority is 0 (catches unmatched fields)

Section Reference

AddressPickerDynamicSection

Trigger: group:address* Priority: 1

Google Places autocomplete with manual entry fallback.

{
"street_number": {
"type": "string",
"tags": ["group:address"],
"priority": 1
},
"route": { "type": "string", "tags": ["group:address"], "priority": 2 },
"locality": { "type": "string", "tags": ["group:address"], "priority": 3 },
"country": { "type": "string", "tags": ["group:address"], "priority": 4 },
"postal_code": { "type": "string", "tags": ["group:address"], "priority": 5 }
}

Supported fields: street_number, route, subpremise, premise, sublocality, sublocality_level_1-5, locality, country, postal_code, postal_code_suffix, administrative_area_level_1-5, geolocation


DatePickerDynamicSection

Trigger: group:date_picker* Priority: 1

Single date picker UI synced to hidden day/month/year fields.

{
"birth_day": {
"type": "integer",
"minimum": 1,
"maximum": 31,
"tags": ["group:date_picker:birth", "date_component:day"]
},
"birth_month": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"tags": ["group:date_picker:birth", "date_component:month"]
},
"birth_year": {
"type": "integer",
"minimum": 1900,
"maximum": 2024,
"tags": ["group:date_picker:birth", "date_component:year"]
}
}

ImageDynamicSection

Trigger: group:images* (create mode only) Priority: 1

Document capture with OCR extraction.

{
"tags": ["action:verify"],
"properties": {
"front_image": {
"type": "string",
"format": "uri",
"contentMediaType": "image/jpeg",
"contentEncoding": "base64",
"tags": ["group:images"],
"image_silhouette": "https://assets.rayt.io/silhouettes/license-front.png"
},
"extracted_name": {
"type": "string",
"tags": ["group:images"],
"readOnly": true
}
}
}

WizardPage threshold options:

  • extract_threshold: Confidence threshold (0-1)
  • extract_threshold_pass_action: "review" | "next_step" | "capture" | "capture_review"
  • extract_threshold_fail_action: Same options
  • extract_threshold_null_action: Same options

QrCodeDynamicSection

Trigger: group:display_qr_code Priority: 1

Multi-step QR code flow for document verification.

{
"tags": ["action:display_qr_code:extract:start"],
"properties": {
"identity_document_type": {
"type": "string",
"enum": ["P", "D"],
"tags": ["group:display_qr_code"]
}
}
}

Reverification Flow:

To enable QR code reverification for an existing ProfileObject, add the action:reverify:display_qr_code schema tag:

{
"tags": [
"action:verify",
"action:display_qr_code:extract:start",
"action:reverify:display_qr_code"
]
}

When this tag is present:

  • Users can reverify existing POs via QR code scan
  • All existing verifications for the PO are expired before creating a new one

PersonNameDynamicSection

Trigger: group:name* Priority: 1

Combined name picker from existing profile objects.

{
"given_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 1
},
"middle_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 2
},
"family_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 3
}
}

CameraDynamicSection

Trigger: group:camera Priority: 2

Photo capture via webcam.

{
"tags": ["default_camera:front"],
"properties": {
"selfie": {
"type": "string",
"format": "uri",
"contentMediaType": "image/jpeg",
"tags": ["group:camera"]
}
}
}

SelectPODynamicSection

Trigger: group:selectProfileObject:* or group:findProfileObject:* Priority: 1

Displays the user's existing ProfileObjects (of the specified schema type) as selectable radio cards. Useful when a form needs to reference data the user has already saved.

{
"linked_identity": {
"type": "object",
"tags": ["group:selectProfileObject:ss_Global_Identity_Document"]
}
}
  • If only one matching PO exists, it auto-selects
  • If none exist, shows a "Create" button linking to that schema
  • Works with action:use_$ref tag to pre-fill other fields from the selected PO

Other Dynamic Sections

SectionTriggerDescription
YodleeDynamicSectiongroup:yodlee-accountsYodlee bank linking
AkahuDynamicSectiongroup:akahu-oauthNZ bank linking
OAuthDynamicSectiongroup:oauth2*Generic OAuth flow
StripeDynamicSectiongroup:stripe_elementsStripe payment
CovidDynamicSectiongroup:covidCOVID pass scanning

8. Conditional Logic

8.1 Which Approach to Use

                    ┌─────────────────────────────┐
│ Do you need different field │
│ DEFINITIONS per condition? │
│ (enum, validation, tags) │
└─────────────────────────────┘

┌──────────┴──────────┐
│ │
YES NO
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Schema-Level │ │ Field-Level │
│ allOf with │ │ if property │
│ if/then │ │ │
└──────────────────┘ └──────────────────┘
Creates separate Simple show/hide
fields with Same field definition
<=> suffix
FeatureField-Level ifSchema-Level allOf
Where definedOn individual fieldAt schema root
What it controlsVisibility onlyProperties, enums, required, verified_fields
Creates new fieldsNoYes (with <=> condition suffix)
Use caseSimple show/hideDifferent field definitions per condition

8.2 Field-Level Conditions (Simple)

Use if directly on a field for simple show/hide:

{
"document_type": {
"type": "string",
"enum": ["passport", "driver_license"]
},
"passport_number": {
"type": "string",
"if": { "document_type": ["passport"] }
},
"license_number": {
"type": "string",
"if": { "document_type": ["driver_license"] }
}
}

Logic: Field shown if trigger field equals ANY of the listed values.

Multiple conditions (AND):

{
"special_field": {
"if": {
"document_type": ["passport"],
"country": ["NZ", "AU"]
}
}
}

This means: (document_type = "passport") AND (country = "NZ" OR country = "AU")

8.3 Schema-Level allOf

Use allOf when you need different field definitions per condition:

{
"properties": {
"identity_document_type": {
"type": "string",
"enum": ["D", "P"],
"tags": ["group:display_qr_code"]
},
"identity_document_data_source": {
"type": "string",
"enum": ["chip", "app", "camera"],
"tags": ["group:display_qr_code"]
}
},
"allOf": [
{
"if": { "properties": { "identity_document_type": { "enum": ["P"] } } },
"then": {
"properties": {
"identity_document_data_source": {
"enum": ["chip"]
}
}
}
},
{
"if": { "properties": { "identity_document_type": { "enum": ["D"] } } },
"then": {
"properties": {
"identity_document_data_source": {
"enum": ["chip", "app"]
}
}
}
}
]
}

What happens:

  1. Build time: Creates conditional fields with <=> condition suffix
  2. Runtime: Shows appropriate field based on current selection

The <=> condition suffix format:

fieldName <=> trigger1=value1 & trigger2=value2|value3

8.4 Conditional Required Fields

Make fields conditionally required:

{
"required": [
"always_required_field",
{
"field": "sometimes_required",
"if": { "trigger_field": ["trigger_value"] }
}
]
}

Example:

{
"properties": {
"has_middle_name": { "type": "boolean" },
"middle_name": { "type": "string" }
},
"required": [
{
"field": "middle_name",
"if": { "has_middle_name": [true] }
}
]
}

8.5 Runtime Conditional Evaluation

The client supports runtime evaluation of conditional schema properties and tags. This includes:

Conditional Tags

Conditional tags in allOf blocks are now evaluated at runtime based on current form values:

{
"allOf": [
{
"if": {
"properties": { "identity_document_type": { "enum": ["EU-PID"] } }
},
"then": {
"tags": ["action:display_global_idv_app_qr_code:extract"]
}
}
]
}

Behavior: The tag action:display_global_idv_app_qr_code:extract is only active when identity_document_type === "EU-PID". This enables conditional UI behaviors like showing App Store QR codes only for specific document types.

Conditional Field Properties

Conditional field properties (title, enum, description, lookup) are merged at runtime:

{
"properties": {
"identity_document_data_source": {
"type": "string",
"title": "Data Source",
"enum": ["app", "chip", "nfc"]
}
},
"allOf": [
{
"if": { "properties": { "identity_document_type": { "enum": ["P"] } } },
"then": {
"properties": {
"identity_document_data_source": {
"title": "PASSPORT: App or Chip Reader",
"enum": ["app", "chip"]
}
}
}
},
{
"if": { "properties": { "identity_document_type": { "enum": ["D"] } } },
"then": {
"properties": {
"identity_document_data_source": {
"title": "DRIVERS LICENSE: NFC Only",
"enum": ["nfc"]
}
}
}
}
]
}

Behavior: When the user selects "P" (Passport), the data source field shows title "PASSPORT: App or Chip Reader" with only "app" and "chip" options. When "D" (Driver License) is selected, it shows "DRIVERS LICENSE: NFC Only" with only "nfc".

How It Works

The client uses evaluateSchemaConditions() to process allOf blocks at runtime:

  1. Form values trigger re-evaluation when they change
  2. Matching conditionals have their then properties deep-merged into the base schema
  3. Tags from matching conditionals are concatenated with base tags
  4. Components access the evaluated schema instead of the static schema

9. Verification

9.1 When /verify is Called

The /verify API is called only when ALL conditions are true:

  1. ✅ Schema has action:verify tag
  2. verified_fields array is NOT empty
  3. dontVerify is NOT set (WizardPage config)
  4. ✅ At least one verifiable field has a value

Common mistake: Having action:verify but no verified_fields:

// ❌ WRONG - /verify will NOT be called
{
"tags": ["action:verify"],
"properties": { "doc_number": { "type": "string" } }
}

// ✅ CORRECT - /verify WILL be called
{
"tags": ["action:verify"],
"properties": { "doc_number": { "type": "string" } },
"verified_fields": ["doc_number"]
}

Disabling Verification

Per WizardPage:

{
"pages": [
{
"schemas": ["ss_Driver_License"],
"verify_data": false
}
]
}

Application Objects: Schemas with type:application_object tag automatically skip verification.

9.2 Conditional Verification

Make verification conditional using allOf:

{
"tags": ["action:verify"],
"properties": {
"verification_level": {
"type": "string",
"enum": ["basic", "enhanced"]
},
"document_number": { "type": "string" },
"expiry_date": { "type": "string", "format": "date" }
},
"verified_fields": ["document_number"],
"allOf": [
{
"if": {
"properties": { "verification_level": { "enum": ["enhanced"] } }
},
"then": {
"verified_fields": ["document_number", "expiry_date"]
}
}
]
}

Result:

  • verification_level = "basic" → Only document_number verified
  • verification_level = "enhanced" → Both fields verified

10. Validation

10.1 Required Fields

Always required:

{
"required": ["field1", "field2"]
}

Conditionally required:

{
"required": [
"always_required",
{ "field": "sometimes_required", "if": { "trigger": ["value"] } }
]
}

10.2 String Validation

{
"license_number": {
"type": "string",
"pattern": "^[A-Z]{2}\\d{6}$",
"patternMessage": "Must be 2 letters + 6 digits (e.g., AB123456)",
"minLength": 8,
"maxLength": 8
}
}

10.3 Number Validation

{
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150
},
"price": {
"type": "number",
"minimum": 0.01,
"maximum": 1000000,
"step": 0.01
}
}

10.4 Boolean Validation (Must Accept)

When a boolean is required, user must check the checkbox:

{
"properties": {
"accept_terms": {
"type": "boolean",
"content": "I accept the terms and conditions"
}
},
"required": ["accept_terms"]
}

11. Lookups

11.1 Enum vs Lookup

Both enum and lookup provide options for selection fields, but they work differently:

Featureenumlookup
Data sourceInline in schemaExternal URL (JSON file)
Display valuesKey = display valueSeparate key and value properties
Extra dataNonedescription, image_src, etc.
Rendering≤7 = radio buttons, >7 = dropdownAlways searchable dropdown
CachingN/ACached by URL

When to Use Each

ScenarioUse
Small fixed list (yes/no, status)enum
Large list (countries, categories)lookup
Need display labels ≠ stored valueslookup
Need descriptions or iconslookup
Shared options across schemaslookup
Quick prototypeenum
Options change without schema updateslookup

11.2 How Enum and Lookup Interact

In standard form fields, enum and lookup are alternatives, not combined.

Priority order for standard dropdowns:

  1. If lookup is present → use lookup options (enum is ignored)
  2. If no lookup but enum exists → use enum values as options

Practical implication:

// ❌ This does NOT filter lookup by enum in standard dropdowns
{
"country": {
"type": "string",
"enum": ["NZ", "AU"],
"lookup": "https://api.rayt.io/lookups/countries.json"
}
}
// Result: Shows ALL countries from lookup (enum is ignored)

// ✅ Use only enum for fixed options
{
"country": {
"type": "string",
"enum": ["NZ", "AU"]
}
}
// Result: Shows "NZ" and "AU" as both key and display value

// ✅ Use only lookup for dynamic options with display values
{
"country": {
"type": "string",
"lookup": "https://api.rayt.io/lookups/countries.json"
}
}
// Result: Shows all countries with proper display names

Enum and Lookup Interaction (All Form Fields)

When both enum and lookup are defined on a field, the following behavior applies consistently across all form fields (standard Select fields and QrCodeDynamicSection's SelectionFlow):

Behavior:

  1. If enum values exist in the lookup → show only matching lookup items (filtered)
  2. If enum values do NOT exist in the lookup → show ALL lookup items (fallback)
  3. If only lookup is defined → show all lookup items
  4. If only enum is defined → show enum values as options

This ensures that invalid enum values don't result in showing raw enum keys. Instead, the full lookup is displayed, which provides a better user experience when there's a schema configuration mismatch.

Example:

{
"country": {
"type": "string",
"enum": ["NZ", "AU"],
"lookup": "{API_DOCS_URL}/lookups/countries.json"
}
}

If countries.json contains [{key: "NZ", value: "New Zealand"}, {key: "AU", value: "Australia"}, {key: "US", value: "United States"}]:

  • Shows only "New Zealand" and "Australia" (filtered by enum)

If enum contained invalid values like ["invalid_1", "invalid_2"]:

  • Shows all countries (fallback to full lookup)

This allows conditional allOf blocks to use different lookup URLs per condition while gracefully handling configuration errors.

11.3 Basic Lookup

{
"country": {
"type": "string",
"lookup": "https://api.rayt.io/lookups/countries.json"
}
}

11.4 Lookup JSON Format

[
{ "key": "NZ", "value": "New Zealand" },
{ "key": "AU", "value": "Australia" }
]

11.5 Extended Lookup Properties

[
{
"key": "full_access",
"value": "Full Account Access",
"description": "Access all account features",
"image_src": "https://...",
"requires_2FA": true
}
]

11.6 Cascading Lookups

For hierarchical data:

{
"category": {
"type": "string",
"tags": ["display:cascade"],
"lookup": "https://api.rayt.io/lookups/categories.json"
}
}

Lookup format:

[
{
"key": "electronics",
"value": "Electronics",
"children": [
{ "key": "phones", "value": "Phones" },
{ "key": "laptops", "value": "Laptops" }
]
}
]

11.7 Lookup Expiry and Auto-Refresh

For time-sensitive data (like real-time pricing, stock availability, or session-based options), use the action:timeout:N tag to automatically refresh lookup data after a specified number of seconds.

Basic Usage

{
"account_balance": {
"type": "string",
"title": "Select Account",
"lookup": "https://api.example.com/accounts.json",
"tags": ["action:timeout:60"]
}
}

This will:

  1. Fetch lookup data initially when the form loads
  2. Display a countdown indicator inside the select field
  3. Automatically refresh the lookup data after 60 seconds
  4. Disable the select field during refresh to prevent stale data selection

Visual Behavior

Normal state: A circular countdown indicator appears inside the select field (to the left of the dropdown arrow). Hovering shows a tooltip with time remaining (e.g., "Refreshes in 45s" or "Refreshes in 1:30").

During refresh: The select is disabled and shows a spinning indicator with an overlay. Users cannot interact with the field until fresh data loads.

Normal state:
┌─────────────────────────────────────┐
│ Select an account ◔ ▼ │ ← countdown circle indicator
└─────────────────────────────────────┘

During refresh:
┌─────────────────────────────────────┐
│ ◌ Loading... 🔄 ▼ │ ← spinning indicator, field disabled
└─────────────────────────────────────┘

Tag Format

TagDescription
action:timeout:30Refresh lookup every 30 seconds
action:timeout:60Refresh lookup every 60 seconds
action:timeout:300Refresh lookup every 5 minutes (300s)

Notes:

  • The timeout value is in seconds
  • Countdown restarts after each successful refresh
  • If the lookup fetch fails, the existing options remain available
  • The countdown indicator cycles through 60-second segments (e.g., 90 seconds shows as 1:30, cycling the indicator every minute)

Use Cases

ScenarioRecommended Timeout
Real-time pricing/stock30-60 seconds
Session-based data60-120 seconds
Infrequently changing data300+ seconds

Example: Bank Account Selection

{
"title": "Payment Details",
"properties": {
"source_account": {
"type": "string",
"title": "Pay From Account",
"description": "Select the account to pay from (balances refresh automatically)",
"lookup": "https://api.bank.example.com/accounts.json",
"tags": ["action:timeout:60"]
}
},
"required": ["source_account"]
}

The lookup JSON might return:

[
{ "key": "acc_001", "value": "Everyday Account - $1,234.56" },
{ "key": "acc_002", "value": "Savings Account - $5,678.90" }
]

With action:timeout:60, the account balances will refresh every minute, ensuring users see up-to-date information.

11.8 Dynamic Lookup URLs with Variables

Lookup URLs can include dynamic variables that are substituted at runtime based on form field values or application context. This enables filtering lookup options based on user selections.

Variable Syntax

PatternSourceExample
{field_name}Form field value{country}
{field.nested}Nested object property{address.city}
{field[0]}Array element{selected_items[0]}
{$context.name}Application context{$context.organizationId}

Available Context Values

Context VariableDescription
{$context.organizationId}Current organization ID
{$context.userId}Current user ID
{$context.tenantId}Current tenant ID
{$context.interfaceId}Current interface/wizard ID

Basic Example: Filter by Parent Field

{
"properties": {
"country": {
"type": "string",
"title": "Country",
"lookup": "https://api.example.com/countries.json"
},
"city": {
"type": "string",
"title": "City",
"lookup": "https://api.example.com/cities.json?country={country}"
}
}
}

When the user selects "NZ" for country, the city lookup URL becomes: https://api.example.com/cities.json?country=NZ

Example: Filter by Organization Context

{
"schema_list": {
"type": "string",
"title": "Select Schema",
"lookup": "https://api.example.com/schemas?org={$context.organizationId}&aa_id={aa_id}"
}
}

This URL substitutes both a context value (organizationId) and a form field value (aa_id).

Behavior

  • Automatic refetch: When a referenced field value changes, the lookup is automatically refetched with the new URL
  • Cache invalidation: Each unique resolved URL is cached separately
  • Error handling: If a variable cannot be resolved (field not found or empty), an error message is displayed instead of the dropdown

Error States

ScenarioBehavior
Variable field not foundShows error: "Unable to load options: field 'X' not found"
Variable resolves to nullShows error (variable must have a value)
Variable resolves to empty stringAllowed (substitutes empty string)
Variable resolves to arrayJoined with comma: ["a","b"]"a,b"
Variable resolves to objectJSON stringified

Array Value Handling

When a variable references an array field, values are joined with commas:

{
"selected_tags": {
"type": "array",
"items": { "type": "string" }
},
"filtered_items": {
"type": "string",
"lookup": "https://api.example.com/items?tags={selected_tags}"
}
}

If selected_tags is ["red", "blue"], the URL becomes: https://api.example.com/items?tags=red,blue

11.9 Client-Side Field Filtering

Use the $field: prefix to create a dependent field whose options are filtered based on another field's selected values. This is useful for "select primary from selected items" patterns.

Syntax

{
"dependent_field": {
"type": "string",
"lookup": "$field:source_field_name"
}
}

Example: Select Primary from Multi-Select

{
"properties": {
"party_uses": {
"type": "array",
"title": "Party Uses",
"items": { "type": "string" },
"lookup": "https://api.example.com/party-uses.json"
},
"party_use_primary": {
"type": "string",
"title": "Primary Party Use",
"lookup": "$field:party_uses"
}
}
}

Behavior:

  1. User selects multiple values in party_uses (e.g., "Residential", "Commercial")
  2. party_use_primary dropdown shows only those selected values
  3. User can pick one as the "primary" option

Conditional Display

It's recommended to hide the dependent field until the source field has selections:

{
"party_use_primary": {
"type": "string",
"title": "Primary Party Use",
"lookup": "$field:party_uses",
"if": {
"party_uses": { "not": { "const": [] } }
}
}
}

Placeholder When No Selection

If the source field has no selections, the dependent field displays a placeholder message:

"Select party_uses first"

Automatic Value Clearing

When the source field changes:

  1. The dependent field's options are updated
  2. If the current value is no longer valid (not in new options), it is automatically cleared
  3. If the current value is still valid, it is preserved

Error States

ScenarioBehavior
Source field has no selectionsShows placeholder: "Select {source_field} first"
Source field has no lookupShows error: "Source field 'X' has no lookup defined"
Selected value becomes invalidValue is automatically cleared

Use Cases

PatternExample
Primary from multi-selectSelect primary contact from selected contacts
Default from optionsSelect default address from selected addresses
Lead item from selectionSelect lead party use from party uses

11.10 Lookup Response Transformation

When the API returns data in a format different from the standard { key, value }[] lookup format, use lookupTransform to transform the response at runtime.

Standard Lookup Format

The lookup system expects responses in this format:

[
{ "key": "option1", "value": "Option 1" },
{ "key": "option2", "value": "Option 2" }
]

When to Use lookupTransform

Use lookupTransform when:

  • Querying PostgREST endpoints that return nested data
  • The API returns arrays inside objects
  • Keys and display values need to be extracted from object properties

Simple Path Extraction

Extract an array from a nested path, using each value as both key and value:

{
"scopes": {
"type": "string",
"title": "Scopes",
"lookup": "{API_BASE_URL}/db/v1/dsm_access_applications?select=scopes&id=eq.{p_aa_id}",
"lookupTransform": "[0].scopes"
}
}

API Response:

[{ "scopes": ["ss_File", "ss_Signature", "ss_Email_Address"] }]

Transformed Result:

[
{ "key": "ss_File", "value": "ss_File" },
{ "key": "ss_Signature", "value": "ss_Signature" },
{ "key": "ss_Email_Address", "value": "ss_Email_Address" }
]

Object Config with Key/Value Mapping

For more control, specify which properties to use as key and value:

{
"category": {
"type": "string",
"title": "Category",
"lookup": "{API_BASE_URL}/db/v1/categories?select=items&parent_id=eq.{parent_id}",
"lookupTransform": {
"path": "[0].items",
"key": "id",
"value": "name"
}
}
}

API Response:

[
{
"items": [
{ "id": "cat_1", "name": "Electronics" },
{ "id": "cat_2", "name": "Furniture" }
]
}
]

Transformed Result:

[
{ "key": "cat_1", "value": "Electronics" },
{ "key": "cat_2", "value": "Furniture" }
]

Path Syntax

The path supports both dot notation and array index notation:

PathDescription
[0].scopesFirst array element, then scopes property
data.itemsNested object properties
results[1].data.listMixed array indices and properties

Configuration Options

PropertyTypeRequiredDescription
pathstringYesJSON path to extract the array
keystringNoProperty name to use as lookup key
valuestringNoProperty name to use as display value

Behavior:

  • If only path is specified: Each array item becomes both key and value
  • If only key is specified: Uses that property for both key and value
  • If only value is specified: Uses that property for both key and value
  • If both key and value are specified: Uses respective properties

Use with Dynamic URLs

lookupTransform works seamlessly with dynamic URL variables:

{
"available_scopes": {
"type": "string",
"title": "Available Scopes",
"lookup": "{API_BASE_URL}/db/v1/dsm_access_applications?select=scopes&id=eq.{p_aa_id}",
"lookupTransform": "[0].scopes"
}
}

When p_aa_id changes, the lookup is refetched and the transformation is applied to the new response.

Error Handling

ScenarioBehavior
Path doesn't exist in responseReturns empty options []
Path doesn't resolve to an arrayReturns empty options []
Missing key/value propertiesUses empty string ""

12. Internationalization (i18n)

12.1 Schema-Level i18n

{
"i18n": {
"en": {
"$schema": {
"title": "Person",
"title_plural": "People",
"description": "Basic person information"
}
},
"mi": {
"$schema": {
"title": "Tangata",
"title_plural": "Tāngata"
}
}
}
}

12.2 Field-Level i18n

{
"i18n": {
"en": {
"given_name": {
"title": "First Name",
"description": "Your legal first name"
}
}
}
}

12.3 Group-Level i18n

{
"i18n": {
"en": {
"group:date_picker:birth.field": {
"title": "Date of Birth",
"description": "Enter your date of birth"
}
}
}
}

12.4 Loading Message i18n

All available loading state keys:

KeyDescription
$loading_extractExtracting data from document
$loading_verifyVerifying information
$loading_saveSaving data
$loading_updateUpdating information
$loading_uploadUploading file
$loading_create_sub_objCreating sub-object
$loading_permissionSetting permissions
$loading_link_to_personLinking to person
$loading_delete_pending_verDeleting old pending verifications
$loading_pending_ver_resubmitPending verification resubmit
$loading_long_verification_messageLong verification (title + description)
{
"i18n": {
"en": {
"$loading_extract": { "title": "Reading your document..." },
"$loading_verify": { "title": "Verifying your identity..." },
"$loading_long_verification_message": {
"title": "Verification in progress",
"description": "This may take a few minutes..."
}
}
}
}

12.5 Selection Flow i18n (QrCodeDynamicSection)

{
"i18n": {
"en": {
"$selection_document_type_header": { "title": "Select Document Type" },
"$selection_document_type_description": {
"title": "Choose the type of ID"
},
"$selection_data_source_header": { "title": "Verification Method" },
"$selection_data_source_description": { "title": "How to verify" },
"$selection_credential_format_header": { "title": "Credential Format" }
}
}
}

13. Wizard Page Configuration

13.1 WizardPage Properties

PropertyTypeDescription
namestringPage title (defaults to schema title)
schemasstring[]Schema names for this page
filter"oneOf"|"anyOf"Single or multiple schema selection
descriptionstringFallback description
description_selectstringDescription when selecting
description_createstringDescription when creating
description_updatestringDescription when updating
optionalbooleanPage can be skipped
multiplebooleanAllow multiple selections
field_liststring[]Whitelist of fields to show
optional_fieldsstring[]Fields not required
verify_databooleanVerify data (default: true)
allow_uploadbooleanAllow file upload
extract_thresholdnumberOCR confidence (0-1)
display_schema_descriptionbooleanShow schema description
display_field_titlebooleanShow field labels
display_field_descriptionbooleanShow field help text
display_modestringTheme: "light", "dark", "default"
branching_rule_namestringConditional page rule ID
selection_hierarchy_jsonstringURL to hierarchical selection config
PropertyTypeDescription
a_idstringAccess Application ID (required)
pagesarrayPage definitions (required)
review_textstringText on review page
submit_textstringSubmit button label
expiry_datenumberDays until link expires
terms_schemastringTerms wizard schema
return_tostringCallback URL
quick_onboardbooleanPasswordless signup

13.3 Hierarchical Schema Selection

When a wizard page has multiple schemas to choose from (using filter: "oneOf"), you can provide a hierarchical navigation structure using selection_hierarchy_json. This is useful when users need to select from many document types organized into categories.

Basic Usage

{
"filter": "oneOf",
"schemas": [
"ss_NZ_DriverLicence",
"ss_AU_NSW_DriverLicence",
"ss_NZ_Passport"
],
"selection_hierarchy_json": "https://api-docs.rayt.io/lookups/raytio_id_document_hierarchy.json"
}

Hierarchy JSON Structure (DeepConfig)

The JSON file defines a recursive tree structure:

interface Option {
option?: string; // ID for i18n lookup
title?: string; // Display title (fallback if no i18n)
description?: string; // Help text (fallback if no i18n)
icon?: string; // URL to icon image
options?: Option[]; // Nested children (for categories)
schema?: SchemaName; // Terminal schema (for leaf nodes)
}

interface DeepConfig {
title?: string;
description?: string;
i18n?: {
[locale: string]: {
[optionId: string]: { title?: string; description?: string };
};
};
options: Option[];
}

Example Hierarchy JSON

{
"title": "Select your document",
"i18n": {
"en": {
"nz": { "title": "New Zealand", "description": "NZ-issued documents" },
"au": { "title": "Australia", "description": "Australian documents" },
"nz_dl": { "title": "Driver Licence" },
"nz_pp": { "title": "Passport" }
},
"mi": {
"nz": { "title": "Aotearoa", "description": "Tuhinga nō Aotearoa" },
"nz_dl": { "title": "Raihana Taraiwa" },
"nz_pp": { "title": "Uruwhenua" }
}
},
"options": [
{
"option": "nz",
"icon": "https://assets.rayt.io/flags/nz.png",
"options": [
{ "option": "nz_dl", "schema": "ss_NZ_DriverLicence" },
{ "option": "nz_pp", "schema": "ss_NZ_Passport" }
]
},
{
"option": "au",
"icon": "https://assets.rayt.io/flags/au.png",
"options": [
{ "title": "NSW Driver Licence", "schema": "ss_AU_NSW_DriverLicence" }
]
}
]
}

In this example:

  • NZ options use option IDs (nz_dl, nz_pp) that reference i18n translations, enabling multi-language support (English and Māori)
  • AU options use inline title directly, which is simpler but not translatable

How It Works

  1. The SelectSchemaList component fetches the JSON from selection_hierarchy_json URL
  2. DeepButtons renders the hierarchy as a recursive timeline navigation
  3. Users drill down through categories until they reach a leaf node with a schema property
  4. The selected schema is then used for form rendering

Notes

  • The hierarchy JSON is fetched directly (not through the lookups API)
  • i18n is optional; if not provided, title/description from Option are used
  • Locale matching finds the best available locale based on the user's browser settings
  • Icons are optional and displayed as small images on category buttons

14. Common Patterns

14.1 Person with Name Picker

{
"title": "Person",
"properties": {
"given_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 1
},
"family_name": {
"type": "string",
"tags": ["group:name:person"],
"priority": 2
},
"email": {
"type": "string",
"pattern": "^[^@]+@[^@]+\\.[^@]+$"
}
},
"required": ["given_name", "family_name", "email"]
}

14.2 Address with Google Places

{
"title": "Address",
"properties": {
"route": { "type": "string", "tags": ["group:address"], "priority": 1 },
"locality": { "type": "string", "tags": ["group:address"], "priority": 2 },
"country": { "type": "string", "tags": ["group:address"], "priority": 3 },
"postal_code": {
"type": "string",
"tags": ["group:address"],
"priority": 4
}
},
"required": ["locality", "country"]
}

14.3 Document with Verification

{
"title": "Driver License",
"tags": ["action:verify", "default_camera:rear"],
"properties": {
"front_image": {
"type": "string",
"contentMediaType": "image/jpeg",
"contentEncoding": "base64",
"tags": ["group:images"],
"image_silhouette": "https://assets.rayt.io/silhouettes/nz-license-front.png"
},
"license_number": {
"type": "string",
"tags": ["group:images"],
"readOnly": true
}
},
"verified_fields": ["license_number"]
}

14.4 Conditional Fields

{
"title": "Identity Document",
"tags": ["action:verify"],
"properties": {
"document_type": {
"type": "string",
"enum": ["P", "D"]
},
"document_number": {
"type": "string"
}
},
"allOf": [
{
"if": { "properties": { "document_type": { "enum": ["P"] } } },
"then": {
"properties": {
"document_number": {
"pattern": "^[A-Z]{2}\\d{6}$",
"patternMessage": "Passport: 2 letters + 6 digits"
}
},
"verified_fields": ["document_number"]
}
}
],
"required": ["document_type", "document_number"]
}