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.

OptionTypeDefaultDescription
requiredbooleanfalseMakes field required. When false, the validator is wrapped in v.optional(...).
defaultValueunknown—Default value when creating new documents. Should match the field’s type.
readOnlybooleanfalseRenders the input as disabled in the admin UI.
placeholderstring—Placeholder text shown in the input.
labelstring | (data) => stringAuto-generated from field nameCustom label. Function form receives the current document data.
descriptionstring—Help text displayed below the label.
conditionFieldCondition—Show/hide the field based on other field values. Conditional fields are always optional in the schema regardless of required.
copyablebooleanfalseShow a copy-to-clipboard button next to the field value.
tooltipstring | { icon?: "info" | "warning" | "danger", text: string }—Tooltip icon next to label. String shorthand uses info icon.
sidebarbooleanfalsePlace the field in the document sidebar instead of the main content area.
sidebarSectionstring—Group under a named sidebar section. Built-in sections: "document", "meta", "actions".
sidebarCollapsedbooleanfalseStart the sidebar section collapsed.
listColumnbooleanfalseShow this field as a default column in the collection list view.
listColumnWidth"auto" | "small" | "medium" | "large"—Column width preset in the list view.
searchablebooleanfalseIndex this field for full-text search.
viewTransitionbooleanfalseEnable CSS View Transition animation for this field.
readRolesstring[]—Roles that can see this field. If unset, all roles can read.
writeRolesstring[]—Roles that can edit this field. If unset, all roles can write.
hiddenbooleanfalseCompletely hide the field from users who lack read access.
uniquebooleanfalseEnforce uniqueness. Adds a by_fieldName Convex index and rejects create/update when another document has the same value. f.slug() defaults to true.
indexedbooleanfalseIdentical to unique. Adds a Convex index and enforces uniqueness.
hooksFieldHooks—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: afterRead field hooks run after the document is fetched, before collection-level afterRead.
  • Writes: field beforeChange runs first, then collection beforeChange, then the database write, then field afterChange, then collection afterChange.

Return Values

  • beforeChange: Return a value to transform the field before writing. Return undefined or void to leave unchanged.
  • afterRead: Return a value to transform the field for the client. Return undefined or void to leave unchanged.
  • afterChange: No return value. Used for side effects (logging, triggering actions).
  • beforeDuplicate: Return a transformed value for the duplicated document. Return undefined or void to 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 defaultValue option 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:
OptionTypeDefaultDescription
rowsnumber—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() when output: "json" (default), v.string() when output: "html".
  • Type-specific options:
OptionTypeDefaultDescription
output"json" | "html""json"Output format. JSON stores TipTap document structure; HTML stores rendered string.
minLinesnumber—Minimum editor height in lines.
maxLinesnumber—Maximum editor height in lines. When 1, editor is single-line with inline marks only.
toolbarRichTextToolbar—Toolbar configuration object.
allowRelativeLinksboolean—Allow relative URLs in the link dialog.

RichTextToolbar

Each toolbar feature can be boolean (enable/disable) or { roles: string[] } (role-gated).

FeatureTypeDescription
boldboolean | { roles: string[] }Bold inline mark.
italicboolean | { roles: string[] }Italic inline mark.
strikethroughboolean | { roles: string[] }Strikethrough inline mark.
underlineboolean | { roles: string[] }Underline inline mark.
codeboolean | { roles: string[] }Inline code mark.
headingsfalse | number[] | { roles?: string[]; levels?: number[] }Heading levels. false disables. Array like [1,2,3,4] specifies allowed levels. Object form adds role gating.
bulletListboolean | { roles: string[] }Unordered list.
orderedListboolean | { roles: string[] }Ordered list.
blockquoteboolean | { roles: string[] }Blockquote block.
codeBlockboolean | { roles: string[] }Fenced code block.
horizontalRuleboolean | { roles: string[] }Horizontal rule.
linkboolean | { roles: string[] }Link insertion.
undoRedoboolean | { roles: string[] }Undo/redo buttons.
sourceViewboolean | { 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:
OptionTypeDefaultDescription
minnumber—Minimum allowed value.
maxnumber—Maximum allowed value.
stepnumber—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:
OptionTypeDefaultDescription
sourceFieldstring—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:
OptionTypeDefaultDescription
localestring—Locale string for formatting (e.g., "en-US").
hourCycle12 | 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(...), ...)) when multiple: true.
  • Type-specific options:
OptionTypeDefaultDescription
optionsArray<string | { label: string; value: string }>requiredAvailable options.
multiplebooleanfalseAllow 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:
OptionTypeDefaultDescription
optionsArray<string | { label: string; value: string }>requiredAvailable 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)) when multiple: true or hasMany: true.
  • Signature: f.id(tableName: string, options?)
  • Type-specific options:
OptionTypeDefaultDescription
multiplebooleanfalseAllow selecting multiple related documents.
hasManybooleanfalseAlias for multiple.
minItemsnumber—Minimum number of selected items (when multiple).
maxItemsnumber—Maximum number of selected items (when multiple).
pickerFilterFieldCondition—Filter condition for the relationship picker dropdown.
pickerSort{ field: string; direction?: "asc" | "desc" }—Sort order in the relationship picker.
displayAsstring | function—Field name or function to determine how related documents are displayed.
scopeFilterboolean—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 the fields definition.
  • Type-specific options:
OptionTypeDefaultDescription
fieldsRecord<string, VextroFieldInput>requiredField definitions for each row.
minRowsnumber—Minimum number of rows.
maxRowsnumber—Maximum number of rows.
rowLabelstring | function | object—Label for each row. String uses a field name, function receives row data, object provides config.
collapsibleboolean—Allow rows to be collapsed.
collapsedboolean—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 the fields definition.
  • Type-specific options:
OptionTypeDefaultDescription
fieldsRecord<string, VextroFieldInput>requiredField definitions for the group.
labelstring—Group label.
descriptionstring—Group description text.
collapsibleboolean—Allow the group to be collapsed.
collapsedboolean—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 with blockType, blockId, and order fields plus block-specific data.
  • Signature: f.blocks(definitions: BlockTypeSpecifier[], options?)
  • Type-specific options:
OptionTypeDefaultDescription
includeOrderbooleantrueInclude 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:
OptionTypeDefaultDescription
languageCodeLanguage | "auto"—Syntax highlighting language. "auto" attempts auto-detection.
lineNumbersbooleantrueShow line numbers.
heightnumber200Editor height in pixels.
wordWrapboolean—Enable word wrapping.
tabSizenumber2Tab size in spaces.
copyButtonboolean—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/geospatial spatial index. Set spatialIndex: false for display-only coordinates.
  • Type-specific options:
OptionTypeDefaultDescription
showMapbooleantrueDisplay an interactive map for point selection.
defaultZoomnumber10Default map zoom level.
defaultCenter{ lat: number; lng: number }—Default map center coordinates.
spatialIndexbooleantrueSync to @convex-dev/geospatial spatial index on create/update/delete.
filterKeysRecord<string, string>{}Map of metadata key → top-level document field name. Indexed as filter keys for spatial queries.
sortKeystring—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)

  1. Install: npm install @convex-dev/geospatial
  2. Register component in convex/convex.config.ts:
    import geospatial from "@convex-dev/geospatial/convex.config";
    const app = defineApp();
    app.use(geospatial);
    app.use(vextro);
  3. 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()) when hasMany: true.
  • Type-specific options:
OptionTypeDefaultDescription
relationTostringrequiredUpload collection slug to store files in.
mimeTypesstring[]—Allowed MIME types (e.g., ["image/png", "application/pdf"]).
maxFileSizenumber—Maximum file size in bytes.
displayPreviewbooleantrueShow a file preview in the admin UI.
hasManyboolean—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 relationTo is set: same as f.upload(). Without relationTo: v.string().
  • Type-specific options:
OptionTypeDefaultDescription
relationTostring—Upload collection slug. When set, uses upload infrastructure with image/* filter.
acceptstring[]—Override accepted MIME types.
maxSizenumber—Maximum file size in bytes.
displayPreviewboolean—Show image preview in admin.
hasManyboolean—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:
OptionTypeDefaultDescription
formatsArray<"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:
OptionTypeDefaultDescription
siteUrlstring—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:
OptionTypeDefaultDescription
tabsTabDefinition[]requiredArray of tab definitions.
defaultTabnumber—Index of the initially active tab.
conditionFieldCondition—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:
OptionTypeDefaultDescription
fieldsRecord<string, VextroFieldInput>requiredFields to display in the row.
widthsnumber[]—Relative widths for each field (e.g., [2, 1] for 2:1 ratio).
gapnumber16Gap between fields in pixels.
conditionFieldCondition—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:
OptionTypeDefaultDescription
labelstringrequiredPanel label.
descriptionstring—Description text below the label.
fieldsRecord<string, VextroFieldInput>requiredFields inside the collapsible.
collapsedboolean—Start collapsed.
conditionFieldCondition—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:
OptionTypeDefaultDescription
labelstringrequiredSection heading text.
descriptionstring—Description text below the heading.
iconstring—Icon identifier for the section header.
collapsiblebooleantrueWhether the section can be collapsed.
collapsedboolean—Start collapsed.
conditionFieldCondition—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:
OptionTypeDefaultDescription
collectionstringrequiredThe related collection slug.
onstringrequiredField or dot-path in the related collection that references this document. Supports group paths (manager.employee) and array paths (designers.employee).
labelstring—Display label.
descriptionstring—Description text.
displayFieldstring—Field to display for each related document.
limitnumber50Maximum number of related documents to fetch.
indexstring—Convex index to use for the reverse lookup.
throughFieldstring—Field name for many-to-many through-table joins.
throughCollectionstring—Through-table collection slug for many-to-many.
conditionFieldCondition—Show/hide the join.
sidebarboolean—Place in document sidebar.
sidebarSectionstring—Sidebar section name.
sidebarCollapsedboolean—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:
OptionTypeDefaultDescription
type"text" | "number" | "url" | "json" | "datetime"requiredThe display type for the computed value.
computestring—Expression or field reference for computation.
labelstring—Display label.
descriptionstring—Description text.
conditionFieldCondition—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:
OptionTypeDefaultDescription
componentstringrequiredName of a registered custom component.
propsRecord<string, unknown>—Props to pass to the component.
watchFieldsstring[]—Field names to watch for changes and re-render the component.
labelstring—Display label.
conditionFieldCondition—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

BuilderConvex ValidatorNotes
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()NoneLayout-only; fields flatten to parent
f.row()NoneLayout-only; fields flatten to parent
f.collapsible()NoneLayout-only; fields flatten to parent
f.section()NoneUI-only; no fields
f.join()NoneVirtual; read-only reverse relationship
f.virtual()NoneVirtual; read-only computed field
f.ui()NonePresentational; no data
f.custom()User-provided validatorWraps any Convex validator

All validators are wrapped in v.optional(...) when required is false (the default) or when a condition is set.

Previous
Styling System