LLM Reference
LLM Reference: Fields & Builders
This is a dense reference for LLMs. It documents every field builder in the f namespace, shared options, conditional logic, field hooks, and the exact Convex validator each builder produces.
CommonFieldOptions
All field builders inherit these options. Any field-type-specific options are documented per builder below.
| Option | Type | Default | Description |
|---|---|---|---|
required | boolean | false | Makes field required. When false, the validator is wrapped in v.optional(...). |
defaultValue | unknown | — | Default value when creating new documents. Should match the field’s type. |
readOnly | boolean | false | Renders the input as disabled in the admin UI. |
placeholder | string | — | Placeholder text shown in the input. |
label | string | (data) => string | Auto-generated from field name | Custom label. Function form receives the current document data. |
description | string | — | Help text displayed below the label. |
condition | FieldCondition | — | Show/hide the field based on other field values. Conditional fields are always optional in the schema regardless of required. |
copyable | boolean | false | Show a copy-to-clipboard button next to the field value. |
tooltip | string | { icon?: "info" | "warning" | "danger", text: string } | — | Tooltip icon next to label. String shorthand uses info icon. |
sidebar | boolean | false | Place the field in the document sidebar instead of the main content area. |
sidebarSection | string | — | Group under a named sidebar section. Built-in sections: "document", "meta", "actions". |
sidebarCollapsed | boolean | false | Start the sidebar section collapsed. |
listColumn | boolean | false | Show this field as a default column in the collection list view. |
listColumnWidth | "auto" | "small" | "medium" | "large" | — | Column width preset in the list view. |
searchable | boolean | false | Index this field for full-text search. |
viewTransition | boolean | false | Enable CSS View Transition animation for this field. |
readRoles | string[] | — | Roles that can see this field. If unset, all roles can read. |
writeRoles | string[] | — | Roles that can edit this field. If unset, all roles can write. |
hidden | boolean | false | Completely hide the field from users who lack read access. |
unique | boolean | false | Enforce uniqueness. Adds a by_fieldName Convex index and rejects create/update when another document has the same value. f.slug() defaults to true. |
indexed | boolean | false | Identical to unique. Adds a Convex index and enforces uniqueness. |
hooks | FieldHooks | — | Field-level hooks. See FieldHooks section below. |
FieldCondition
FieldCondition is a union type that controls field visibility based on document state.
Object Forms
// Equals check — show field when another field has a specific value
{ field: string; equals: unknown }
// In-array check — show field when another field's value is in the array
{ field: string; in: unknown[] }
// Logical AND — all conditions must be true
{ and: FieldCondition[] }
// Logical OR — at least one condition must be true
{ or: FieldCondition[] }
// Logical NOT — condition must be false
{ not: FieldCondition }
Function Form
(args: { data: Record<string, unknown>; siblingData: Record<string, unknown>; parentTree: Record<string, unknown>[] }) => boolean
The function form receives the full document data, sibling field data (same level in nested structures), and the parent tree for deeply nested fields.
Important: Any field with a condition is always wrapped in v.optional(...) in the generated schema, regardless of the required option. This ensures the schema remains valid when the field is hidden and no value is provided.
FieldHooks
Field-level hooks run during read and write operations for individual fields.
type FieldHooks = {
beforeChange?: Array<(args: {
value: unknown;
originalValue?: unknown;
data: Record<string, unknown>;
originalDoc?: Record<string, unknown>;
operation: "create" | "update";
ctx: GenericMutationCtx;
fieldName: string;
collection: CollectionDefinition;
collectionSlug: string;
context: HookContext;
}) => Promise<Value | undefined | void> | Value | undefined | void>;
afterRead?: Array<(args: {
value: unknown;
doc: Record<string, unknown>;
ctx: GenericQueryCtx;
fieldName: string;
collection: CollectionDefinition;
collectionSlug: string;
context: HookContext;
}) => Promise<Value | undefined | void> | Value | undefined | void>;
afterChange?: Array<(args: {
value: unknown;
previousValue: unknown;
doc: Record<string, unknown>;
operation: "create" | "update";
fieldName: string;
context: HookContext;
ctx: GenericMutationCtx;
collection: CollectionDefinition;
collectionSlug: string;
}) => Promise<void> | void>;
beforeDuplicate?: Array<(args: {
value: unknown;
siblingData: Record<string, unknown>;
fieldName: string;
context: HookContext;
ctx: GenericMutationCtx;
collection: CollectionDefinition;
collectionSlug: string;
}) => Promise<Value | undefined | void> | Value | undefined | void>;
};
Hook Execution Order
- Reads:
afterReadfield hooks run after the document is fetched, before collection-levelafterRead. - Writes: field
beforeChangeruns first, then collectionbeforeChange, then the database write, then fieldafterChange, then collectionafterChange.
Return Values
beforeChange: Return a value to transform the field before writing. Returnundefinedorvoidto leave unchanged.afterRead: Return a value to transform the field for the client. Returnundefinedorvoidto leave unchanged.afterChange: No return value. Used for side effects (logging, triggering actions).beforeDuplicate: Return a transformed value for the duplicated document. Returnundefinedorvoidto keep the original value.
HookContext
HookContext is a mutable Record<string, unknown> shared across all hooks in a single mutation or query. Use it to pass data between hooks without globals.
Field Type Reference
f.text()
Plain text input.
- Convex validator:
v.string() - Type-specific options: None. The shared
defaultValueoption accepts a boolean and initializes new documents before conditions are evaluated.
title: f.text({ required: true, placeholder: "Enter title" })
f.textarea()
Multi-line text input.
- Convex validator:
v.string() - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
rows | number | — | Number of visible text rows. |
body: f.textarea({ rows: 10, placeholder: "Write content..." })
f.richText()
Rich text editor powered by TipTap.
- Convex validator:
v.any()whenoutput: "json"(default),v.string()whenoutput: "html". - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
output | "json" | "html" | "json" | Output format. JSON stores TipTap document structure; HTML stores rendered string. |
minLines | number | — | Minimum editor height in lines. |
maxLines | number | — | Maximum editor height in lines. When 1, editor is single-line with inline marks only. |
toolbar | RichTextToolbar | — | Toolbar configuration object. |
allowRelativeLinks | boolean | — | Allow relative URLs in the link dialog. |
RichTextToolbar
Each toolbar feature can be boolean (enable/disable) or { roles: string[] } (role-gated).
| Feature | Type | Description |
|---|---|---|
bold | boolean | { roles: string[] } | Bold inline mark. |
italic | boolean | { roles: string[] } | Italic inline mark. |
strikethrough | boolean | { roles: string[] } | Strikethrough inline mark. |
underline | boolean | { roles: string[] } | Underline inline mark. |
code | boolean | { roles: string[] } | Inline code mark. |
headings | false | number[] | { roles?: string[]; levels?: number[] } | Heading levels. false disables. Array like [1,2,3,4] specifies allowed levels. Object form adds role gating. |
bulletList | boolean | { roles: string[] } | Unordered list. |
orderedList | boolean | { roles: string[] } | Ordered list. |
blockquote | boolean | { roles: string[] } | Blockquote block. |
codeBlock | boolean | { roles: string[] } | Fenced code block. |
horizontalRule | boolean | { roles: string[] } | Horizontal rule. |
link | boolean | { roles: string[] } | Link insertion. |
undoRedo | boolean | { roles: string[] } | Undo/redo buttons. |
sourceView | boolean | { roles: string[] } | Raw source view toggle. |
content: f.richText({
output: "json",
toolbar: { headings: [1, 2, 3], bold: true, italic: true, link: true },
minLines: 5,
})
f.number()
Numeric input.
- Convex validator:
v.number() - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
min | number | — | Minimum allowed value. |
max | number | — | Maximum allowed value. |
step | number | — | Step increment for the input. |
price: f.number({ required: true, min: 0, step: 0.01 })
f.email()
Email address input with validation.
- Convex validator:
v.string() - Type-specific options: None.
contactEmail: f.email({ required: true, placeholder: "user@example.com" })
f.url()
URL input with validation.
- Convex validator:
v.string() - Type-specific options: None.
websiteUrl: f.url({ placeholder: "https://example.com" })
f.slug()
URL-safe slug input with optional auto-generation.
- Convex validator:
v.string() - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
sourceField | string | — | Field name to auto-generate the slug from (e.g., "title"). |
slug: f.slug({ required: true, sourceField: "title" })
f.date()
Date picker. Stores as ISO 8601 date string.
- Convex validator:
v.string() - Type-specific options: None.
publishDate: f.date({ required: true, label: "Publish Date" })
f.datetime()
Date and time picker. Stores as Unix milliseconds.
- Convex validator:
v.number() - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
locale | string | — | Locale string for formatting (e.g., "en-US"). |
hourCycle | 12 | 24 | — | 12-hour or 24-hour clock display. |
scheduledAt: f.datetime({ required: true, hourCycle: 24 })
f.select()
Dropdown select input. Supports single and multiple selection.
- Convex validator:
v.union(v.literal(...), ...)for single select.v.array(v.union(v.literal(...), ...))whenmultiple: true. - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
options | Array<string | { label: string; value: string }> | required | Available options. |
multiple | boolean | false | Allow selecting multiple values. |
status: f.select({
required: true,
options: [
{ label: "Draft", value: "draft" },
{ label: "Published", value: "published" },
{ label: "Scheduled", value: "scheduled" },
{ label: "Trashed", value: "trashed" },
],
})
f.radio()
Radio button group. Single selection only.
- Convex validator:
v.union(v.literal(...), ...)(same as single select). - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
options | Array<string | { label: string; value: string }> | required | Available options. |
layout | "horizontal" | "vertical" | "vertical" | Layout direction for radio buttons. |
Note: placeholder is not applicable for radio fields.
priority: f.radio({
options: ["low", "medium", "high"],
layout: "horizontal",
})
f.checkbox()
Boolean checkbox input.
- Convex validator:
v.boolean() - Type-specific options: None.
Note: placeholder is not applicable for checkbox fields.
isFeatured: f.checkbox({ label: "Featured", defaultValue: true, sidebar: true })
f.id()
Relationship field linking to another collection’s documents.
- Convex validator:
v.id(tableName)for single.v.array(v.id(tableName))whenmultiple: trueorhasMany: true. - Signature:
f.id(tableName: string, options?) - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
multiple | boolean | false | Allow selecting multiple related documents. |
hasMany | boolean | false | Alias for multiple. |
minItems | number | — | Minimum number of selected items (when multiple). |
maxItems | number | — | Maximum number of selected items (when multiple). |
pickerFilter | FieldCondition | — | Filter condition for the relationship picker dropdown. |
pickerSort | { field: string; direction?: "asc" | "desc" } | — | Sort order in the relationship picker. |
displayAs | string | function | — | Field name or function to determine how related documents are displayed. |
scopeFilter | boolean | — | Filter related documents by the active scope. |
author: f.id("users", { required: true, displayAs: "displayName" })
tags: f.id("tags", { hasMany: true, maxItems: 10 })
f.array()
Repeatable row group. Stores as an array of objects.
- Convex validator:
v.array(v.object({...}))where the object shape is derived from thefieldsdefinition. - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
fields | Record<string, VextroFieldInput> | required | Field definitions for each row. |
minRows | number | — | Minimum number of rows. |
maxRows | number | — | Maximum number of rows. |
rowLabel | string | function | object | — | Label for each row. String uses a field name, function receives row data, object provides config. |
collapsible | boolean | — | Allow rows to be collapsed. |
collapsed | boolean | — | Start rows collapsed. |
Note: placeholder is not applicable for array fields.
links: f.array({
fields: {
label: f.text({ required: true }),
url: f.url({ required: true }),
isExternal: f.checkbox(),
},
maxRows: 5,
rowLabel: "label",
})
f.group()
Nested object group. Stores as a nested object within the parent document.
- Convex validator:
v.object({...})where the object shape is derived from thefieldsdefinition. - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
fields | Record<string, VextroFieldInput> | required | Field definitions for the group. |
label | string | — | Group label. |
description | string | — | Group description text. |
collapsible | boolean | — | Allow the group to be collapsed. |
collapsed | boolean | — | Start the group collapsed. |
Note: placeholder is not applicable for group fields.
address: f.group({
label: "Address",
collapsible: true,
fields: {
street: f.text({ required: true }),
city: f.text({ required: true }),
state: f.text(),
zip: f.text(),
},
})
f.blocks()
Block-based content area. Stores as an array of typed block objects.
- Convex validator:
v.array(v.union(v.object({ blockType: v.literal("..."), blockId: v.id("..."), order: v.number(), ... }), ...))— one union member per block type, each withblockType,blockId, andorderfields plus block-specific data. - Signature:
f.blocks(definitions: BlockTypeSpecifier[], options?) - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
includeOrder | boolean | true | Include an order field on each block for sorting. |
Note: placeholder is not applicable for block fields.
pageContent: f.blocks([HeroBlock, ContentBlock, CTABlock], {
includeOrder: true,
})
f.json()
Raw JSON editor.
- Convex validator:
v.any() - Type-specific options: None.
metadata: f.json({ label: "Custom Metadata" })
f.code()
Code editor with syntax highlighting.
- Convex validator:
v.string() - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
language | CodeLanguage | "auto" | — | Syntax highlighting language. "auto" attempts auto-detection. |
lineNumbers | boolean | true | Show line numbers. |
height | number | 200 | Editor height in pixels. |
wordWrap | boolean | — | Enable word wrapping. |
tabSize | number | 2 | Tab size in spaces. |
copyButton | boolean | — | Show a copy button. |
CodeLanguage Values
"javascript" | "typescript" | "json" | "html" | "css" | "scss" | "markdown" | "yaml" | "xml" | "sql" | "graphql" | "python" | "ruby" | "go" | "rust" | "java" | "php" | "c" | "cpp" | "csharp" | "shell" | "plaintext"
snippet: f.code({ language: "typescript", height: 300, lineNumbers: true })
f.point()
Geographic coordinate input with automatic spatial indexing via @convex-dev/geospatial.
- Convex validator:
v.object({ lat: v.number(), lng: v.number() }) - Spatial indexing: Enabled by default. On document create/update/delete, Vextro automatically syncs point data to a
@convex-dev/geospatialspatial index. SetspatialIndex: falsefor display-only coordinates. - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
showMap | boolean | true | Display an interactive map for point selection. |
defaultZoom | number | 10 | Default map zoom level. |
defaultCenter | { lat: number; lng: number } | — | Default map center coordinates. |
spatialIndex | boolean | true | Sync to @convex-dev/geospatial spatial index on create/update/delete. |
filterKeys | Record<string, string> | {} | Map of metadata key → top-level document field name. Indexed as filter keys for spatial queries. |
sortKey | string | — | Top-level document field name to use as the sort key in the spatial index. |
Note: placeholder is not applicable for point fields.
Setup checklist (required when spatialIndex is true)
- Install:
npm install @convex-dev/geospatial - Register component in
convex/convex.config.ts:import geospatial from "@convex-dev/geospatial/convex.config"; const app = defineApp(); app.use(geospatial); app.use(vextro); - Pass component to admin module in
convex/admin.ts:const admin = createVextroAdminModule({ query, mutation, components, geospatialComponent: components.geospatial, collectionDefinitions: vextro.collectionDefinitions, });
If geospatialComponent is not passed but any collection has a point field with spatialIndex: true, createVextroAdminModule() throws:
“Collection ‘X’ has a spatially-indexed point field ‘Y’ but no geospatial component was provided.”
Example with spatial indexing
location: f.point({
required: true,
showMap: true,
filterKeys: { category: "category", status: "status" },
sortKey: "rating",
})
Example without spatial indexing
headquartersPin: f.point({ spatialIndex: false })
Querying the spatial index
Vextro handles insert/update/remove automatically. For queries, use the GeospatialIndex API directly:
import { createSpatialIndex } from "vextro/convex/geo";
import { components } from "./_generated/api";
const geoIndex = createSpatialIndex(components.geospatial);
// Bounding box
const results = await geoIndex.query(ctx, {
shape: { type: "rectangle", rectangle: { west: -74.1, east: -73.9, south: 40.6, north: 40.8 } },
filter: (q) => q.eq("category", "restaurant"),
});
// Nearest neighbor
const nearest = await geoIndex.nearest(ctx, {
point: { latitude: 40.7128, longitude: -74.006 },
filter: (q) => q.eq("status", "published"),
limit: 10,
});
Utility: haversineDistance
For display distances (not queries), import from vextro/convex/geo:
import { haversineDistance } from "vextro/convex/geo";
const miles = haversineDistance({ lat: 40.71, lng: -74.01 }, { lat: 34.05, lng: -118.24 }, "mi");
f.upload()
File upload with relationship to an upload collection.
- Convex validator:
v.string()for single.v.array(v.string())whenhasMany: true. - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
relationTo | string | required | Upload collection slug to store files in. |
mimeTypes | string[] | — | Allowed MIME types (e.g., ["image/png", "application/pdf"]). |
maxFileSize | number | — | Maximum file size in bytes. |
displayPreview | boolean | true | Show a file preview in the admin UI. |
hasMany | boolean | — | Allow multiple file uploads. |
attachment: f.upload({ relationTo: "media", mimeTypes: ["application/pdf"] })
f.image()
Image upload field. Delegates to f.upload() when relationTo is set, adding an image/* MIME type filter. Without relationTo, stores a plain string (legacy mode).
- Convex validator: When
relationTois set: same asf.upload(). WithoutrelationTo:v.string(). - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
relationTo | string | — | Upload collection slug. When set, uses upload infrastructure with image/* filter. |
accept | string[] | — | Override accepted MIME types. |
maxSize | number | — | Maximum file size in bytes. |
displayPreview | boolean | — | Show image preview in admin. |
hasMany | boolean | — | Allow multiple images. |
heroImage: f.image({ relationTo: "media", required: true })
gallery: f.image({ relationTo: "media", hasMany: true })
f.file()
Generic file upload. Same as f.image() but without default MIME type filtering.
- Convex validator: Same as
f.image(). - Type-specific options: Same as
f.image().
document: f.file({ relationTo: "media", accept: ["application/pdf", "text/csv"] })
f.color()
Color picker input. Stores a CSS color string. Optional color fields remain unset until a color is chosen, and clearing them restores the unset state.
- Convex validator:
v.string() - Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
formats | Array<"hex" | "rgb" | "hsl" | "oklch"> | — | Allowed color formats in the picker. |
brandColor: f.color({ formats: ["hex", "rgb"], required: true })
f.seo()
SEO metadata field group. Renders as a structured set of SEO inputs.
- Convex validator:
v.object({
metaTitle: v.optional(v.string()),
metaDescription: v.optional(v.string()),
canonicalUrl: v.optional(v.string()),
ogTitle: v.optional(v.string()),
ogDescription: v.optional(v.string()),
ogImage: v.optional(v.string()),
})
- Type-specific options:
| Option | Type | Default | Description |
|---|---|---|---|
siteUrl | string | — | Base site URL for preview and canonical URL generation. |
Note: placeholder is not applicable for SEO fields.
seo: f.seo({ siteUrl: "https://example.com" })
f.tabs()
Layout container that organizes fields into tabbed panels. No data storage — child fields are flattened to the parent document.
- Convex validator: None (layout-only). Fields within tabs produce validators at the parent level.
- Options:
| Option | Type | Default | Description |
|---|---|---|---|
tabs | TabDefinition[] | required | Array of tab definitions. |
defaultTab | number | — | Index of the initially active tab. |
condition | FieldCondition | — | Show/hide the entire tab container. |
TabDefinition
type TabDefinition = {
label: string;
description?: string;
fields: Record<string, VextroFieldInput>;
condition?: FieldCondition;
};
layout: f.tabs({
tabs: [
{
label: "Content",
fields: {
title: f.text({ required: true }),
body: f.richText(),
},
},
{
label: "Settings",
fields: {
slug: f.slug({ sourceField: "title" }),
status: f.select({ options: ["draft", "published"] }),
},
},
],
})
f.row()
Layout container that places fields in a horizontal row. No data storage — fields flatten to the parent.
- Convex validator: None (layout-only).
- Options:
| Option | Type | Default | Description |
|---|---|---|---|
fields | Record<string, VextroFieldInput> | required | Fields to display in the row. |
widths | number[] | — | Relative widths for each field (e.g., [2, 1] for 2:1 ratio). |
gap | number | 16 | Gap between fields in pixels. |
condition | FieldCondition | — | Show/hide the row. |
nameRow: f.row({
fields: {
firstName: f.text({ required: true }),
lastName: f.text({ required: true }),
},
widths: [1, 1],
})
f.collapsible()
Layout container that wraps fields in a collapsible panel. No data storage — fields flatten to the parent.
- Convex validator: None (layout-only).
- Options:
| Option | Type | Default | Description |
|---|---|---|---|
label | string | required | Panel label. |
description | string | — | Description text below the label. |
fields | Record<string, VextroFieldInput> | required | Fields inside the collapsible. |
collapsed | boolean | — | Start collapsed. |
condition | FieldCondition | — | Show/hide the collapsible. |
advanced: f.collapsible({
label: "Advanced Options",
collapsed: true,
fields: {
customCSS: f.code({ language: "css" }),
jsonConfig: f.json(),
},
})
f.section()
UI-only divider or header for visual grouping. No data storage.
- Convex validator: None (UI-only).
- Options:
| Option | Type | Default | Description |
|---|---|---|---|
label | string | required | Section heading text. |
description | string | — | Description text below the heading. |
icon | string | — | Icon identifier for the section header. |
collapsible | boolean | true | Whether the section can be collapsed. |
collapsed | boolean | — | Start collapsed. |
condition | FieldCondition | — | Show/hide the section. |
divider: f.section({ label: "Media & Assets", description: "Upload images and files" })
f.join()
Virtual reverse relationship. Displays related documents from another collection that reference this document. Not stored — computed at read time.
- Convex validator: None (virtual, read-only).
- Options:
| Option | Type | Default | Description |
|---|---|---|---|
collection | string | required | The related collection slug. |
on | string | required | Field or dot-path in the related collection that references this document. Supports group paths (manager.employee) and array paths (designers.employee). |
label | string | — | Display label. |
description | string | — | Description text. |
displayField | string | — | Field to display for each related document. |
limit | number | 50 | Maximum number of related documents to fetch. |
index | string | — | Convex index to use for the reverse lookup. |
throughField | string | — | Field name for many-to-many through-table joins. |
throughCollection | string | — | Through-table collection slug for many-to-many. |
condition | FieldCondition | — | Show/hide the join. |
sidebar | boolean | — | Place in document sidebar. |
sidebarSection | string | — | Sidebar section name. |
sidebarCollapsed | boolean | — | Start collapsed in sidebar. |
Always read-only.
posts: f.join({
collection: "posts",
on: "author",
displayField: "title",
limit: 20,
})
f.virtual()
Computed field derived from other data. Not stored — computed at read time or display time.
- Convex validator: None (virtual, read-only).
- Options:
| Option | Type | Default | Description |
|---|---|---|---|
type | "text" | "number" | "url" | "json" | "datetime" | required | The display type for the computed value. |
compute | string | — | Expression or field reference for computation. |
label | string | — | Display label. |
description | string | — | Description text. |
condition | FieldCondition | — | Show/hide the virtual field. |
Always read-only.
fullName: f.virtual({
type: "text",
compute: "firstName + ' ' + lastName",
label: "Full Name",
})
f.ui()
Presentational component slot. Renders a registered custom component in the editor. No data stored.
- Convex validator: None (presentational only).
- Options:
| Option | Type | Default | Description |
|---|---|---|---|
component | string | required | Name of a registered custom component. |
props | Record<string, unknown> | — | Props to pass to the component. |
watchFields | string[] | — | Field names to watch for changes and re-render the component. |
label | string | — | Display label. |
condition | FieldCondition | — | Show/hide the component. |
preview: f.ui({
component: "LivePreview",
watchFields: ["title", "body", "heroImage"],
})
f.custom()
Escape hatch for fields with a raw Convex validator and a custom admin field type.
- Convex validator: The validator you pass as the first argument.
- Signature:
f.custom(validator: ConvexValidator, adminType: string, config?)
coordinates: f.custom(
v.object({ street: v.string(), city: v.string(), zip: v.string() }),
"address",
{ label: "Mailing Address" }
)
Validator Mapping Summary
| Builder | Convex Validator | Notes |
|---|---|---|
f.text() | v.string() | |
f.textarea() | v.string() | |
f.richText() | v.any() or v.string() | v.any() for JSON output, v.string() for HTML output |
f.number() | v.number() | |
f.email() | v.string() | |
f.url() | v.string() | |
f.slug() | v.string() | |
f.date() | v.string() | ISO 8601 date string |
f.datetime() | v.number() | Unix milliseconds |
f.select() | v.union(v.literal(...)) | v.array(v.union(...)) when multiple: true |
f.radio() | v.union(v.literal(...)) | Single selection only |
f.checkbox() | v.boolean() | |
f.id() | v.id(tableName) | v.array(v.id(tableName)) when multiple/hasMany |
f.array() | v.array(v.object({...})) | Object shape from fields |
f.group() | v.object({...}) | Object shape from fields |
f.blocks() | v.array(v.union(...)) | Union of block type objects with blockType, blockId, order |
f.json() | v.any() | |
f.code() | v.string() | |
f.point() | v.object({ lat: v.number(), lng: v.number() }) | |
f.upload() | v.string() | v.array(v.string()) when hasMany: true |
f.image() | v.string() | v.array(v.string()) when hasMany: true; plain string without relationTo |
f.file() | v.string() | v.array(v.string()) when hasMany: true |
f.color() | v.string() | CSS color string |
f.seo() | v.object({ metaTitle?, metaDescription?, canonicalUrl?, ogTitle?, ogDescription?, ogImage? }) | All sub-fields are v.optional(v.string()) |
f.tabs() | None | Layout-only; fields flatten to parent |
f.row() | None | Layout-only; fields flatten to parent |
f.collapsible() | None | Layout-only; fields flatten to parent |
f.section() | None | UI-only; no fields |
f.join() | None | Virtual; read-only reverse relationship |
f.virtual() | None | Virtual; read-only computed field |
f.ui() | None | Presentational; no data |
f.custom() | User-provided validator | Wraps any Convex validator |
All validators are wrapped in v.optional(...) when required is false (the default) or when a condition is set.