> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Properties

> The property helper system — typed schemas that drive runtime validation, body prop types, editor form controls, and JSON Schema generation for downstream consumers

Properties declare what configurable inputs a component accepts. The same schema drives four things at once: type-safe `props` in the [body](/bindjs/authoring/components#the-body-signature), runtime validation, form-control generation in editors that integrate BindJS, and JSON Schema generation for downstream consumers — validators, MCP tool input checks, and LLMs reasoning about how to invoke a component. You write the schema once; everything else is derived.

## How properties work

The lifecycle is the same in every BindJS runtime:

1. **Definition** — a component declares a `properties` schema in its [`defineComponent`](/bindjs/authoring/components) call. Each entry is built from a property helper such as `PropertyString` or `PropertyEnum`.
2. **Runtime resolution** — at render time the runtime resolves the schema, applies defaults, and produces a typed props object that the body function receives.
3. **Editor integration** — editors integrating BindJS read the schema to render input controls; updates flow back through the runtime as new prop values.
4. **Re-render** — when props change, the body re-runs with the new values.

The type structure of the body's `props` argument is inferred from the schema — there's no manual `ComponentProps` interface to maintain. Change a helper and the body's prop type changes with it.

## Property schema structure

A component's `properties` field is a record from property name to a property-helper call. The helpers compose: array helpers nest a `valueType`, group helpers nest a `properties` block, and component helpers can declare an `allowedComponents` allowlist for slots.

```typescript theme={null}
const properties = {
    title: PropertyString({
        title: "Component Title",
        description: "The main title displayed in the component",
        required: true,
        defaultValue: "Default Title",
        inspector: {
            placeholder: "Enter title...",
            showLabel: true,
            helpDescription: "This title appears at the top of the component",
        },
        validation: { minLength: 1, maxLength: 100 },
    }),

    fontSize: PropertyNumber({
        title: "Font Size",
        description: "Size of the title font in points",
        defaultValue: 16,
        validation: { min: 8, max: 72 },
        inspector: { control: "slider", step: 1, showLabel: true },
    }),

    showIcon: PropertyBoolean({
        title: "Show Icon",
        description: "Whether to display an icon next to the title",
        defaultValue: false,
    }),

    alignment: PropertyEnum({
        title: "Text Alignment",
        description: "How the text should be aligned",
        options: [
            { value: "left", label: "Left" },
            { value: "center", label: "Center" },
            { value: "right", label: "Right" },
        ],
        defaultValue: "left",
        inspector: { control: "segmented" },
    }),
}

export default defineComponent({ properties, body })
```

The `properties` field also accepts a function returning a record, for cases where the schema needs runtime context — for example, looking up enum options from the host.

## Property helpers

Eleven helpers cover the input types BindJS supports: scalars, enums, dates, arrays, groups, host-resolved references (assets and content), and component slots. Every helper accepts the [base fields](#base-fields-all-property-types) (`title`, `description`, `required`, `defaultValue`, `examples`, `inspector`, `validation`) plus its own helper-specific options.

### PropertyString

For text input — single-line, multi-line, code, or markdown.

```typescript theme={null}
PropertyString({
    title: "Text Content",
    description: "The text to display",
    required: true,
    defaultValue: "Default text",
    examples: ["Hello World", "Welcome to our app"],

    validation: {
        minLength: 1,
        maxLength: 500,
        pattern: "^[a-zA-Z0-9 ]+$",
        format: "text",
    },

    inspector: {
        placeholder: "Enter text...",
        control: "singleline",
        showLabel: true,
        helpDescription: "Enter the text you want to display",
    },
})
```

For multi-line text, set `inspector.control: "multiline"` and optionally `numberOfLines`. To enable a markdown formatting toolbar, set `inspector.markdown: true`:

```typescript theme={null}
PropertyString({
    title: "Description",
    description: "Detailed description text",
    inspector: {
        control: "multiline",
        numberOfLines: 5,
        markdown: true,
        placeholder: "Enter description...",
    },
    validation: { maxLength: 1000 },
})
```

For source code input, set `inspector.control: "code"`.

<ParamField path="inspector.control" type="&#x22;singleline&#x22; | &#x22;multiline&#x22; | &#x22;code&#x22;" optional>
  The text input variant. Defaults to `"singleline"`.
</ParamField>

<ParamField path="inspector.placeholder" type="string" optional>
  Placeholder text shown when the field is empty.
</ParamField>

<ParamField path="inspector.markdown" type="boolean" optional>
  Show a markdown formatting toolbar on multi-line fields.
</ParamField>

<ParamField path="inspector.numberOfLines" type="number" optional>
  The visible line count for multi-line fields.
</ParamField>

<ParamField path="validation.minLength" type="number" optional>
  Minimum character length.
</ParamField>

<ParamField path="validation.maxLength" type="number" optional>
  Maximum character length.
</ParamField>

<ParamField path="validation.pattern" type="string" optional>
  A regular expression the value must match.
</ParamField>

<ParamField path="validation.format" type="&#x22;text&#x22; | &#x22;email&#x22; | &#x22;url&#x22;" optional>
  A format constraint applied alongside the pattern check.
</ParamField>

### PropertyNumber

For numeric input accepting any real number, with an `input` field or `slider` control.

```typescript theme={null}
PropertyNumber({
    title: "Width",
    description: "Width in pixels",
    defaultValue: 300,
    validation: { min: 0, max: 1000 },
    inspector: {
        control: "input",
        step: 10,
        placeholder: 300,
        showLabel: true,
        helpDescription: "Width must be between 0 and 1000 pixels",
    },
})
```

<ParamField path="inspector.control" type="&#x22;input&#x22; | &#x22;slider&#x22;" optional>
  The control variant. `"slider"` renders a draggable range; `"input"` renders a numeric field. Defaults to `"input"`.
</ParamField>

<ParamField path="inspector.step" type="number" optional>
  The step increment for the input or slider.
</ParamField>

<ParamField path="inspector.placeholder" type="number" optional>
  A placeholder value shown when the field is empty.
</ParamField>

<ParamField path="validation.min" type="number" optional>
  Minimum allowed value.
</ParamField>

<ParamField path="validation.max" type="number" optional>
  Maximum allowed value.
</ParamField>

### PropertyInteger

The same shape as `PropertyNumber`, but restricted to integer values. The generated JSON Schema emits `{ type: "integer" }` — useful when downstream consumers need an integer guarantee at the schema level (for example, an OpenAPI path parameter typed as `integer`).

```typescript theme={null}
PropertyInteger({
    title: "Page",
    description: "Page number (1-based)",
    defaultValue: 1,
    validation: { min: 1, max: 100 },
    inspector: { control: "input", step: 1 },
})
```

Use `PropertyNumber` when any real number is acceptable; use `PropertyInteger` when the consumer needs an integer guarantee at the schema level.

### PropertyBoolean

For toggle and switch controls.

```typescript theme={null}
PropertyBoolean({
    title: "Enabled",
    description: "Whether the feature is enabled",
    defaultValue: true,
    inspector: {
        showLabel: true,
        helpDescription: "Toggle to enable or disable this feature",
    },
})
```

`PropertyBoolean` has no helper-specific options beyond the base fields.

### PropertyEnum

For selection from a predefined set of options. Options can be plain strings, plain numbers, objects with `value` plus `label` (for text labels), or objects with `value` plus `icon` (for icon-only segmented controls).

**Plain-string options:**

```typescript theme={null}
PropertyEnum({
    title: "Size",
    options: ["small", "medium", "large"],
    defaultValue: "medium",
    inspector: { control: "segmented" },
})
```

**Object options with labels:**

```typescript theme={null}
PropertyEnum({
    title: "Theme",
    description: "Visual theme for the component",
    options: [
        { value: "light", label: "Light Theme" },
        { value: "dark", label: "Dark Theme" },
        { value: "auto", label: "Auto (System)" },
    ],
    defaultValue: "auto",
    required: true,
    inspector: {
        control: "segmented",
        showLabel: true,
        helpDescription: "Choose how the component should appear",
    },
})
```

**Icon-only segmented control:**

```typescript theme={null}
PropertyEnum({
    title: "Alignment",
    options: [
        { value: "leading", icon: "align-left" },
        { value: "center", icon: "align-center" },
        { value: "trailing", icon: "align-right" },
    ],
    defaultValue: "leading",
    inspector: { control: "segmented" },
})
```

<ParamField path="options" type="Array<string | number | { value, label } | { value, icon }>" required>
  The selectable values. Use plain strings/numbers for compact lists, `{ value, label }` for labeled options, or `{ value, icon }` for icon-only segmented controls.
</ParamField>

<ParamField path="inspector.control" type="&#x22;segmented&#x22; | &#x22;dropdown&#x22;" optional>
  The control variant. `"segmented"` renders inline buttons; `"dropdown"` renders a select menu. Defaults to `"dropdown"`.
</ParamField>

### PropertyDate

For date selection, with an ISO 8601 string at runtime.

```typescript theme={null}
PropertyDate({
    title: "Publish Date",
    description: "When to publish the content",
    defaultValue: "2024-01-01",

    validation: {
        minDate: "2024-01-01",
        maxDate: "2025-12-31",
    },

    inspector: {
        placeholder: "Select date...",
        showLabel: true,
        helpDescription: "Must be within the current year",
    },
})
```

<ParamField path="inspector.placeholder" type="string" optional>
  Placeholder text for the date picker.
</ParamField>

<ParamField path="validation.minDate" type="string" optional>
  The minimum allowed date as an ISO 8601 string.
</ParamField>

<ParamField path="validation.maxDate" type="string" optional>
  The maximum allowed date as an ISO 8601 string.
</ParamField>

### PropertyArray

For repeating lists of values. The item type is set by `valueType` — any other property helper.

**Array of strings:**

```typescript theme={null}
PropertyArray({
    title: "Tags",
    description: "Tags for categorization",
    defaultValue: ["new", "featured"],

    valueType: PropertyString({
        title: "Tag",
        validation: { maxLength: 20 },
    }),

    validation: { minItems: 1, maxItems: 10 },

    inspector: {
        helpDescription: "Add up to 10 tags",
        showLabel: true,
    },
})
```

**Array of components:**

```typescript theme={null}
PropertyArray({
    title: "Gallery Images",
    description: "Images to display in the gallery",

    valueType: PropertyComponent({
        title: "Image",
        allowedComponents: ["StoryPhoto"],
        environment: { gallery: true },
    }),

    validation: { maxItems: 20 },
})
```

**Array of grouped objects:**

```typescript theme={null}
PropertyArray({
    title: "Authors",
    valueType: PropertyGroup({
        title: "Author",
        properties: {
            name: PropertyString({ title: "Name", required: true }),
            role: PropertyString({ title: "Role" }),
        },
    }),
})
```

<ParamField path="valueType" type="PropertyHelper" required>
  The item type. Any property helper is allowed, including nested `PropertyArray`, `PropertyGroup`, and `PropertyComponent`.
</ParamField>

<ParamField path="validation.minItems" type="number" optional>
  Minimum number of items.
</ParamField>

<ParamField path="validation.maxItems" type="number" optional>
  Maximum number of items.
</ParamField>

### PropertyAsset

For media file selection. The host's asset system resolves the value to an object with exactly one of `image`, `video`, `audio`, or `model`, each carrying `url`, `dimensions`, `mimeType`, and a host-supplied `id`.

```typescript theme={null}
PropertyAsset({
    title: "Background Image",
    description: "Image to use as background",
    assetTypes: ["image"],
    required: false,
    inspector: {
        showLabel: true,
        helpDescription: "Recommended size: 1920x1080px",
    },
})
```

Multiple types are allowed in the same picker:

```typescript theme={null}
PropertyAsset({
    title: "Media",
    assetTypes: ["image", "video"],
})
```

Reading an asset value inside the body:

```typescript theme={null}
const body = (props, children) =>
    props.asset
        ? Image({ url: props.asset.image.url })
              .resizable()
              .frame({ width: props.asset.image.dimensions.width / 2 })
        : AssetPlaceholder()
```

<ParamField path="assetTypes" type="AssetType[]" required>
  The allowed asset types. Common values: `"image"`, `"video"`, `"audio"`, `"model"`. The host defines the full set.
</ParamField>

### PropertyContent

For referencing another content item. The host resolves the reference through its content system.

```typescript theme={null}
PropertyContent({
    title: "Related Article",
    description: "Link to a related article",
    required: false,
    inspector: {
        showLabel: true,
        helpDescription: "Select an article to link",
    },
})
```

`PropertyContent` has no helper-specific options beyond the base fields. The shape of the resolved value is host-defined.

### PropertyComponent

For embedding a child component. Combine with `allowedComponents` to constrain the slot to a specific set of component types, and with `environment` to pass context values down to the child.

```typescript theme={null}
PropertyComponent({
    title: "Header Component",
    description: "Custom header component",
    allowedComponents: ["SiteHeader", "MinimalHeader"],

    environment: {
        theme: "dark",
        context: "header",
    },

    inspector: { showLabel: true },
})
```

`PropertyComponent` is the foundation for component slots. Wrapping it in `PropertyArray` produces a list slot — see [Slots of child components](#slots-of-child-components) for the patterns.

<ParamField path="allowedComponents" type="string[]" optional>
  Component names allowed in this slot. The host's component registry resolves names; LLM and editor consumers use this list to narrow the candidate set.
</ParamField>

<ParamField path="environment" type="Record<string, any>" optional>
  Environment values to inject into the child's render context. Children read these via [`useEnvironment`](/bindjs/authoring/state).
</ParamField>

### PropertyGroup

For grouping related properties under a single collapsible section. The group's own `properties` field follows the same record-of-helpers structure as a component's top-level schema.

```typescript theme={null}
PropertyGroup({
    title: "Author Information",
    description: "Details about the content author",

    properties: {
        name: PropertyString({
            title: "Name",
            required: true,
            validation: { maxLength: 50 },
        }),
        email: PropertyString({
            title: "Email",
            validation: { format: "email" },
        }),
        bio: PropertyString({
            title: "Biography",
            inspector: {
                control: "multiline",
                numberOfLines: 3,
            },
        }),
        avatar: PropertyAsset({
            title: "Profile Picture",
            assetTypes: ["image"],
        }),
    },

    inspector: { showLabel: true },
})
```

Groups nest. A group's `properties` may include other groups, arrays of groups, or any other helper.

<ParamField path="properties" type="Record<string, PropertyHelper>" required>
  The nested fields. Inferred as a nested object on the body's `props`.
</ParamField>

## Slots of child components

Layout components declare where children can be placed by combining `PropertyComponent` (a single slot) or a `PropertyArray` of `PropertyComponent` (a list slot) with the `allowedComponents` allowlist. The slot's name carries the convention.

### Single section, default slot

The convention is to name the default slot `components` — a list of view components rendered in the layout's main area:

```typescript theme={null}
const properties = {
    title: PropertyString({ title: "Page Title", required: true }),

    components: PropertyArray({
        title: "Page Content",
        description: "Main content components",
        valueType: PropertyComponent({
            allowedComponents: ["Heading", "Paragraph", "Image", "Quote"],
        }),
        inspector: { visible: false },
    }),
}
```

### Multi-section layout

Distinct named slots produce distinct edit regions in editors. Setting `inspector.visible: false` keeps the slot itself out of the inspector — the layout surfaces the children directly inside the canvas instead.

```typescript theme={null}
const properties = {
    sidebar: PropertyArray({
        title: "Sidebar Content",
        valueType: PropertyComponent({
            allowedComponents: ["NavList", "Promo"],
        }),
        validation: { maxItems: 5 },
        inspector: { visible: false },
    }),

    main: PropertyArray({
        title: "Main Content",
        valueType: PropertyComponent({
            allowedComponents: ["Heading", "Paragraph", "Image"],
        }),
        validation: { minItems: 1 },
        inspector: { visible: false },
    }),
}
```

### Domain-specific slot names

For domain-specific layouts, name the slots after the things they contain. The same allowlist mechanism applies — only the named component types are accepted.

```typescript theme={null}
// FAQ Layout
const properties = {
    faqs: PropertyArray({
        title: "FAQ Items",
        valueType: PropertyComponent({ allowedComponents: ["FAQItem"] }),
        inspector: { visible: false },
    }),
}

// Recipe Layout
const properties = {
    ingredients: PropertyArray({
        title: "Ingredients",
        valueType: PropertyComponent({ allowedComponents: ["Ingredient"] }),
        inspector: { visible: false },
    }),
    steps: PropertyArray({
        title: "Instructions",
        valueType: PropertyComponent({ allowedComponents: ["Step"] }),
        inspector: { visible: false },
    }),
}
```

### Single-component slots

For a slot that holds exactly one child rather than a list, drop the `PropertyArray` wrapper:

```typescript theme={null}
const properties = {
    header: PropertyComponent({
        title: "Header",
        allowedComponents: ["SiteHeader", "MinimalHeader"],
    }),
}
```

## Base fields (all property types)

Every property helper accepts the same base fields. Helper-specific options layer on top of these.

| Field          | Type      | Description                                                                     |
| -------------- | --------- | ------------------------------------------------------------------------------- |
| `title`        | `string`  | Display name in editors and the schema's `title`.                               |
| `description`  | `string`  | Natural-language description used by editors, validators, and LLMs.             |
| `required`     | `boolean` | Whether the field is required at validation time.                               |
| `defaultValue` | `T`       | Type-specific default applied when no value is supplied.                        |
| `examples`     | `T[]`     | Example values surfaced in editors and the generated schema.                    |
| `inspector`    | `object`  | Editor configuration — see [Inspector configuration](#inspector-configuration). |
| `validation`   | `object`  | Type-specific validation rules — see [Validation rules](#validation-rules).     |

<Tip>
  Treat `description` as the primary documentation surface. It's what an LLM reads when reasoning about whether and how to populate the field, and what editors render below the input as help text.
</Tip>

## Inspector configuration

The `inspector` object controls how the property appears in editors that render the schema as form controls. Some fields are common to every type; others are type-specific.

```typescript theme={null}
inspector: {
    // Common (all types)
    showLabel: true,             // Whether to show the field label
    showDivider: true,           // Whether to show a divider below the field
    helpDescription: "...",      // Tooltip / info text
    visible: (props) => true,    // Dynamic visibility — return false to hide

    // String-specific
    placeholder: "...",
    control: "singleline",        // "singleline" | "multiline" | "code"
    markdown: false,
    numberOfLines: 3,

    // Number / Integer-specific
    control: "input",             // "input" | "slider"
    step: 1,

    // Enum-specific
    control: "segmented",         // "segmented" | "dropdown"

    // Date-specific
    placeholder: "Select date...",
}
```

### Conditional visibility

`visible` accepts a callback that receives the current resolved props and returns a boolean. Use it to show or hide a field based on the value of another property — for example, only showing a `customColor` field when `colorMode === "custom"`:

```typescript theme={null}
const properties = {
    showAdvanced: PropertyBoolean({
        title: "Show Advanced Options",
        defaultValue: false,
    }),
    advancedSetting: PropertyString({
        title: "Advanced Setting",
        inspector: {
            visible: (props) => props.showAdvanced === true,
        },
    }),

    colorMode: PropertyEnum({
        title: "Color Mode",
        options: ["auto", "custom"],
        defaultValue: "auto",
    }),
    customColor: PropertyString({
        title: "Custom Color",
        inspector: {
            visible: (props) => props.colorMode === "custom",
        },
    }),
}
```

The callback runs whenever the form's resolved props change, so toggling `showAdvanced` immediately reveals or hides `advancedSetting` without a re-render of the parent.

## Validation rules

The `validation` object contains type-specific rules. Editors enforce these at input time; the runtime enforces them when resolving props before each render.

**String:**

```typescript theme={null}
validation: {
    minLength: 1,
    maxLength: 500,
    pattern: "^[a-zA-Z0-9 ]+$",
    format: "text" | "email" | "url",
}
```

**Number / Integer:**

```typescript theme={null}
validation: {
    min: 0,
    max: 100,
}
```

**Array:**

```typescript theme={null}
validation: {
    minItems: 1,
    maxItems: 20,
}
```

**Date:**

```typescript theme={null}
validation: {
    minDate: "2024-01-01",
    maxDate: "2025-12-31",
}
```

`PropertyBoolean`, `PropertyEnum`, `PropertyAsset`, `PropertyContent`, `PropertyComponent`, and `PropertyGroup` don't add helper-specific validation rules beyond the base `required` field. Group validation flows through the nested helpers.

## Property type reference

The full set of helpers, with their runtime and schema mappings:

| Helper              | Runtime type | JSON Schema type       | Notes                                                                         |
| ------------------- | ------------ | ---------------------- | ----------------------------------------------------------------------------- |
| `PropertyString`    | `string`     | `string`               | Text input — `singleline`, `multiline`, or `code` controls.                   |
| `PropertyNumber`    | `number`     | `number`               | Real numbers; `input` or `slider` control.                                    |
| `PropertyInteger`   | `integer`    | `integer`              | Integer-only; same shape as `PropertyNumber`.                                 |
| `PropertyBoolean`   | `boolean`    | `boolean`              | Toggle / switch.                                                              |
| `PropertyEnum`      | `enum`       | `string`/`number` enum | `segmented` or `dropdown` control.                                            |
| `PropertyDate`      | `date`       | `string` (ISO 8601)    | Date picker.                                                                  |
| `PropertyArray`     | `array`      | `array`                | Repeatable list, item type set by `valueType`.                                |
| `PropertyGroup`     | `group`      | nested `object`        | Nested fields under one collapsible section.                                  |
| `PropertyAsset`     | `asset`      | host-defined           | Image / video / audio / 3D model picker; resolved by the host's asset system. |
| `PropertyContent`   | `content`    | host-defined           | Reference to another content item; resolved by the host's content system.     |
| `PropertyComponent` | `component`  | recursive              | Embedded child component, with `allowedComponents` allowlist for recursion.   |

## Schema generation for editors and LLMs

Every property helper has a deterministic mapping to JSON Schema. A consumer — an inspector form, a validator, an LLM — can read a component's `properties` schema and know exactly what a valid props object for that component looks like, without having to execute the body. This is the contract that makes BindJS usable as a target for editor-driven and LLM-generated UI.

### What flows into the generated schema

* **Type structure** — each helper emits a corresponding JSON Schema fragment (`{ type: "string" }`, `{ type: "integer" }`, `{ type: "array", items: ... }`). Nested helpers — `PropertyArray({ valueType: PropertyGroup({...}) })`, for example — compose into nested schema.
* **`title`** — included as the schema's `title`. Used as the field label in editors and as a human-readable identifier for LLM consumers.
* **`description`** — included as the schema's `description`. This is the primary documentation surface for the field — what an LLM sees when reasoning about whether and how to populate it.
* **`required` / `defaultValue`** — included as the schema's `required` array entries and `default` values.
* **`validation`** — `minLength`, `maxLength`, `min`, `max`, `pattern`, `minItems`, `maxItems`, and `uniqueItems` flow through as the equivalent JSON Schema constraints.
* **`examples`** — included as the schema's `examples` array, which both editors and LLMs use as concrete guidance.
* **`inspector` hints** — host-specific (control type, placeholder, help text). Editors that adopt them get richer forms; consumers that don't can ignore them.

### Recursive component schemas

`PropertyComponent` declares a slot for a nested component, optionally constrained to a specific set of component types via `allowedComponents`:

```typescript theme={null}
PropertyComponent({
    title: "Hero",
    allowedComponents: ["StoryPhoto", "StoryVideo", "StoryParagraph"],
})
```

A schema generator walks this recursively — looking up each name in `allowedComponents`, generating a schema for that component's own `properties`, and emitting the full set as a `oneOf` (or equivalent) in the parent's schema. Walk repeatedly and you get a schema for an entire component subtree, rooted at any starting component.

The same applies to `PropertyArray({ valueType: PropertyComponent({ allowedComponents: [...] }) })` — the array's `items` schema is the recursive expansion.

### Why this matters

The combination — typed properties, `description` as natural-language documentation, and `allowedComponents` as a navigable component graph — means a single root component carries enough information for an LLM to generate a valid invocation of the whole subtree, or for a host to validate an LLM's output against the schema before rendering. Three things follow:

* **Editors get inspectors for free.** Any tool that can render a JSON Schema form can render an inspector for any BindJS component, without per-component handwritten UI.
* **LLMs see structured documentation.** The `description` field on every property is the surface the LLM reads when reasoning about how to populate the schema. Treat it as the doc string, not as a label — every property the LLM is supposed to fill should describe its intent in prose.
* **Hosts can validate before rendering.** An LLM's response can be checked against the generated schema before it's passed to a renderer. Invalid output is caught at the edge, not by the renderer crashing on a malformed prop.

### How Metabind uses it

Metabind realizes this contract in two places.

**Content types in MCP App Studio.** When a root BindJS component is bound to a content type in MCP App Studio, the CMS recursively walks the component's `properties`, expanding each `PropertyComponent({ allowedComponents })` into the schemas of its allowed children, and so on. The result is a single JSON Schema that describes a valid content document for the whole subtree. The CMS uses it to render inspector forms, validate edits, and version-lock the schema with the published package — when you publish a new version of a component, content created against the old schema continues to validate against the version it was authored under.

**Interactive Tools in the Cloud MCP App Server.** An Interactive Tool declares an allowlist of components the LLM is permitted to render. The Cloud MCP App Server uses the same recursive walker over those components' `properties` to produce the tool's input schema, which is published as the tool's `inputSchema` in MCP. The LLM sees the schema — including every `description` field — and generates a structured response. The server validates the response against the schema before the props ever reach the renderer.

In both cases, the only thing the consumer needs from your code is the `properties` shape. There's no separate schema file to maintain — your component's typed inputs and the schema editors and LLMs see are the same artifact.

## What to read next

<CardGroup cols={2}>
  <Card title="Components and bodies" icon="cube" href="/bindjs/authoring/components">
    How `properties` fits inside `defineComponent` alongside `body`, `metadata`, and `previews`.
  </Card>

  <Card title="Composition" icon="diagram-project" href="/bindjs/authoring/composition">
    Slots of child components, recursive `Self`, and how component names resolve at runtime.
  </Card>

  <Card title="Talking to the MCP host" icon="messages" href="/bindjs/authoring/mcp-host">
    How a BindJS view calls back into the chat — tool calls, messaging, display modes.
  </Card>

  <Card title="Layout — Stacks" icon="layer-group" href="/bindjs/components/layout-stacks#vstack">
    VStack, HStack, ZStack — the layout primitives a component body returns.
  </Card>
</CardGroup>
