> ## Documentation Index
> Fetch the complete documentation index at: https://forest-chore-open-api.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Forms

> Collect user input with static and dynamic action forms

Action forms let you collect user input before executing an action. Forms can be static with fixed fields, or dynamic with fields that adapt based on context or user input.

<Frame caption="An action form displayed in Forest">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/actions-forms-charge-cc.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=945cc07fc047dea3bdb634251c6e93a9" alt="Action form displayed in Forest" width="1920" height="969" data-path="images/actions/actions-forms-charge-cc.png" />
</Frame>

## Field properties

Fields are configurable using the following properties:

| Property         | Required            | Type      | Description                                                                                                                                             |
| ---------------- | ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **type**         | Yes                 | string    | Field type: `Boolean`, `Date`, `Dateonly`, `Enum`, `Json`, `Number`, `NumberList`, `EnumList`, `String`, `StringList`, `File`, `FileList`, `Collection` |
| **label**        | Yes                 | string    | Label displayed to the user                                                                                                                             |
| **id**           | No                  | string    | Internal identifier. If not set, the label is used. Use this to access values in `context.formValues`                                                   |
| **description**  | No                  | string    | Help text displayed below the field                                                                                                                     |
| **isRequired**   | No                  | boolean   | Make the field required (default: false)                                                                                                                |
| **defaultValue** | No                  | any       | Default value pre-filled in the form                                                                                                                    |
| **isReadOnly**   | No                  | boolean   | Make the field read-only (default: false)                                                                                                               |
| **enumValues**   | Required for `Enum` | string\[] | List of possible values when type is `Enum`                                                                                                             |
| **widget**       | No                  | string    | UI widget to use (see widgets section below)                                                                                                            |

## Basic form example

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  agent.customizeCollection('customers', collection => {
    collection.addAction('Charge credit card', {
      scope: 'Single',
      form: [
        {
          type: 'Number',
          label: 'Amount',
          description: 'Amount in USD (e.g., 42.50)',
          isRequired: true,
        },
        {
          type: 'String',
          label: 'Description',
          widget: 'TextArea',
        },
      ],
      execute: async (context, resultBuilder) => {
        const { Amount, Description } = context.formValues;
        const customer = await context.getRecord(['stripeId']);

        // Charge the credit card
        await stripe.charges.create({
          amount: Amount * 100,
          currency: 'usd',
          customer: customer.stripeId,
          description: Description,
        });

        return resultBuilder.success('Charged successfully!');
      },
    });
  });
  ```

  ```ruby Ruby theme={null}
  forest_agent.customize_collection('customers') do |collection|
    collection.add_action(
      'Charge credit card',
      BaseAction.new(
        scope: ActionScope::SINGLE,
        form: [
          {
            type: FieldType::NUMBER,
            label: 'Amount',
            description: 'Amount in USD (e.g., 42.50)',
            is_required: true
          },
          {
            type: FieldType::STRING,
            label: 'Description',
            widget: 'TextArea'
          }
        ]
      ) do |context, result_builder|
        form_values = context.form_values
        customer = context.get_record(['stripeId'])

        # Charge the credit card
        # ...

        result_builder.success('Charged successfully!')
      end
    )
  end
  ```

  ```ruby Ruby DSL theme={null}
  @create_agent.collection :customers do |collection|
    collection.action 'Charge credit card', scope: :single do
      form do
        field :amount, type: :number, description: 'Amount in USD (e.g., 42.50)', is_required: true
        field :description, type: :string, widget: 'TextArea'
      end

      execute do
        customer = record(['stripeId'])
        amount = form_value(:amount)
        description = form_value(:description)

        # Charge the credit card
        # ...

        success 'Charged successfully!'
      end
    end
  end
  ```
</CodeGroup>

## Field types

### String

Text input for short strings.

```javascript theme={null}
{ type: 'String', label: 'Name' }
```

Use the `TextArea` widget for longer text:

```javascript theme={null}
{ type: 'String', label: 'Description', widget: 'TextArea' }
```

### Number

Numeric input with optional constraints.

```javascript theme={null}
{ type: 'Number', label: 'Amount', defaultValue: 0 }
```

### Boolean

Checkbox for true/false values.

```javascript theme={null}
{ type: 'Boolean', label: 'Send confirmation email' }
```

### Date and Dateonly

Date picker for dates with or without time.

```javascript theme={null}
{ type: 'Date', label: 'Delivery date and time' }
{ type: 'Dateonly', label: 'Birth date' }
```

### Enum

Dropdown with predefined options.

```javascript theme={null}
{
  type: 'Enum',
  label: 'Status',
  enumValues: ['pending', 'approved', 'rejected'],
  isRequired: true
}
```

### Collection

Reference to a record from another collection.

```javascript theme={null}
{
  type: 'Collection',
  label: 'Assignee',
  collectionName: 'users',
  description: 'Select the user to assign this ticket to'
}
```

<Frame caption="Collection reference widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/actions-form-collection.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=e5daca8e5cfdddcecfee41eb39f8055d" alt="Collection reference widget on an action form" width="1074" height="390" data-path="images/actions/actions-form-collection.png" />
</Frame>

The value will be the primary key of the selected record (as an array for composite keys).

### File and FileList

File upload fields.

```javascript theme={null}
{ type: 'File', label: 'Upload document' }
{ type: 'FileList', label: 'Upload attachments' }
```

### Lists

Arrays of values.

```javascript theme={null}
{ type: 'StringList', label: 'Tags' }
{ type: 'NumberList', label: 'Scores' }
{ type: 'EnumList', label: 'Categories', enumValues: ['A', 'B', 'C'] }
```

## Dynamic forms

Make forms reactive by using functions instead of static values. Functions receive the action context and access form values and selected records.

### Dynamic required fields

Make a field required based on another field's value:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  form: [
    {
      type: 'Number',
      label: 'Amount',
      isRequired: true,
    },
    {
      type: 'String',
      label: 'Justification',
      description: 'Required for amounts over $1000',
      // Only required if amount > 1000
      isRequired: context => context.formValues.Amount > 1000,
    },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    {
      type: FieldType::NUMBER,
      label: 'Amount',
      is_required: true
    },
    {
      type: FieldType::STRING,
      label: 'Justification',
      description: 'Required for amounts over $1000',
      # Only required if amount > 1000
      is_required: ->(context) { context.form_values['Amount'] > 1000 }
    }
  ]
  ```
</CodeGroup>

### Conditional visibility

Show or hide fields based on conditions:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  form: [
    {
      type: 'Boolean',
      label: 'Send email notification',
      id: 'sendEmail',
    },
    {
      type: 'String',
      label: 'Email address',
      // Only show if sendEmail is checked
      if: context => context.formValues.sendEmail === true,
    },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    {
      type: FieldType::BOOLEAN,
      label: 'Send email notification',
      id: 'sendEmail'
    },
    {
      type: FieldType::STRING,
      label: 'Email address',
      # Only show if sendEmail is checked
      if: ->(context) { context.form_values['sendEmail'] == true }
    }
  ]
  ```
</CodeGroup>

### Default values from record data

Pre-fill form with data from the selected record:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  form: [
    {
      type: 'Number',
      label: 'Refund amount',
      // Default to the order total
      defaultValue: async context => {
        const order = await context.getRecord(['total']);
        return order.total;
      },
    },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    {
      type: FieldType::NUMBER,
      label: 'Refund amount',
      # Default to the order total
      default_value: ->(context) {
        order = context.get_record(['total'])
        order['total']
      }
    }
  ]
  ```
</CodeGroup>

### Dynamic enum values

Change dropdown options based on context:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  form: [
    {
      type: 'Enum',
      label: 'Department',
      id: 'department',
      enumValues: ['Engineering', 'Sales', 'Support'],
    },
    {
      type: 'Enum',
      label: 'Team',
      // Teams depend on selected department
      enumValues: context => {
        const dept = context.formValues.department;
        if (dept === 'Engineering') return ['Backend', 'Frontend', 'DevOps'];
        if (dept === 'Sales') return ['Inbound', 'Outbound'];
        if (dept === 'Support') return ['L1', 'L2', 'L3'];
        return [];
      },
    },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    {
      type: FieldType::ENUM,
      label: 'Department',
      id: 'department',
      enum_values: ['Engineering', 'Sales', 'Support']
    },
    {
      type: FieldType::ENUM,
      label: 'Team',
      # Teams depend on selected department
      enum_values: ->(context) {
        dept = context.form_values['department']
        if dept == 'Engineering'
          ['Backend', 'Frontend', 'DevOps']
        elsif dept == 'Sales'
          ['Inbound', 'Outbound']
        elsif dept == 'Support'
          ['L1', 'L2', 'L3']
        else
          []
        end
      }
    }
  ]
  ```
</CodeGroup>

### Dynamic collection references

Change the target collection dynamically:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  form: [
    {
      type: 'Enum',
      label: 'Entity type',
      id: 'entityType',
      enumValues: ['user', 'company'],
    },
    {
      type: 'Collection',
      label: 'Entity',
      // Collection name depends on entity type
      collectionName: context => context.formValues.entityType,
    },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    {
      type: FieldType::ENUM,
      label: 'Entity type',
      id: 'entityType',
      enum_values: ['user', 'company']
    },
    {
      type: FieldType::COLLECTION,
      label: 'Entity',
      # Collection name depends on entity type
      collection_name: ->(context) { context.form_values['entityType'] }
    }
  ]
  ```
</CodeGroup>

## Widgets

Widgets customize the UI appearance of fields. Here are the most common ones:

### TextArea

Multi-line text input.

```javascript theme={null}
{ type: 'String', label: 'Notes', widget: 'TextArea' }
```

<Frame caption="TextArea widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-text-area.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=451f74a8374d2b34399161617b15b289" alt="TextArea widget on an action form" width="1348" height="704" data-path="images/actions/action-forms-widget-text-area.png" />
</Frame>

### TextInput

One-line text input, the default widget for `String` fields.

```javascript theme={null}
{ type: 'String', label: 'Name', widget: 'TextInput' }
```

<Frame caption="TextInput widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-text-input.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=07d65efef42ad8fb54ac7eedf19fd73d" alt="TextInput widget on an action form" width="1348" height="370" data-path="images/actions/action-forms-widget-text-input.png" />
</Frame>

### TextInputList

One-line text input to enter a list of string values.

```javascript theme={null}
{ type: 'StringList', label: 'Tags', widget: 'TextInputList' }
```

<Frame caption="TextInputList widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-text-input-list.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=7cdc2c6cfdba36ca60617cef56e590cc" alt="TextInputList widget on an action form" width="1348" height="496" data-path="images/actions/action-forms-widget-text-input-list.png" />
</Frame>

### AddressAutocomplete

Text input with address autocomplete powered by the Google Maps API.

```javascript theme={null}
{
  type: 'String',
  label: 'Address',
  widget: 'AddressAutocomplete',
  placeholder: 'Type the address here'
}
```

<Frame caption="AddressAutocomplete widget, empty state">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-address-autocomplete-empty.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=cd353c60bd02542ba310b5cace64dd85" alt="AddressAutocomplete widget on an action form, empty state" width="1306" height="346" data-path="images/actions/action-forms-widget-address-autocomplete-empty.png" />
</Frame>

<Frame caption="AddressAutocomplete widget, with suggestions">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-address-autocomplete-open.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=64cd59b4724da95041c62bafbe4e96e0" alt="AddressAutocomplete widget on an action form, with suggestions" width="1306" height="738" data-path="images/actions/action-forms-widget-address-autocomplete-open.png" />
</Frame>

### Checkbox

Single checkbox for boolean values.

```javascript theme={null}
{ type: 'Boolean', label: 'Send a notification', widget: 'Checkbox' }
```

<Frame caption="Checkbox widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-checkbox.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=46635be2c0041114390030d6bbf7074a" alt="Checkbox widget on an action form" width="1348" height="370" data-path="images/actions/action-forms-widget-checkbox.png" />
</Frame>

### Dropdown

Alternative to Enum for dropdown selection.

```javascript theme={null}
{
  type: 'String',
  label: 'Priority',
  widget: 'Dropdown',
  options: ['Low', 'Medium', 'High']
}
```

<Frame caption="Dropdown widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/actions-forms-widget-dropdown.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=a3706485b8ecbfa9a2af2465b500173d" alt="Dropdown widget on an action form" width="1348" height="784" data-path="images/actions/actions-forms-widget-dropdown.png" />
</Frame>

### RadioGroup

Radio buttons for single selection.

```javascript theme={null}
{
  type: 'Enum',
  label: 'Plan',
  widget: 'RadioGroup',
  enumValues: ['Free', 'Pro', 'Enterprise']
}
```

<Frame caption="RadioGroup widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-radio-group.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=680efb27256e65830ace0725870e76a3" alt="RadioGroup widget on an action form" width="1302" height="350" data-path="images/actions/action-forms-widget-radio-group.png" />
</Frame>

### CheckboxGroup

Checkboxes for multiple selection.

```javascript theme={null}
{
  type: 'EnumList',
  label: 'Features',
  widget: 'CheckboxGroup',
  enumValues: ['API Access', 'Priority Support', 'Custom Domain']
}
```

### DatePicker

Calendar widget for date selection.

```javascript theme={null}
{ type: 'Date', label: 'Appointment', widget: 'DatePicker' }
```

### TimePicker

Input for entering a time value.

```javascript theme={null}
{ type: 'Time', label: 'Opening time', widget: 'TimePicker' }
```

<Frame caption="TimePicker widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-time-picker.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=09e4bb252c1028a1a02249e453f37b12" alt="TimePicker widget on an action form" width="1298" height="574" data-path="images/actions/action-forms-widget-time-picker.png" />
</Frame>

### ColorPicker

Color selection widget.

```javascript theme={null}
{ type: 'String', label: 'Brand color', widget: 'ColorPicker' }
```

### FilePicker

File upload with preview.

```javascript theme={null}
{ type: 'File', label: 'Profile picture', widget: 'FilePicker' }
```

<Frame caption="FilePicker widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-file-picker.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=dfd6d2ae565ae62fadbaf817bf6e7dc8" alt="FilePicker widget on an action form" width="1312" height="882" data-path="images/actions/action-forms-widget-file-picker.png" />
</Frame>

### JsonEditor

JSON editor with syntax highlighting.

```javascript theme={null}
{ type: 'Json', label: 'Configuration', widget: 'JsonEditor' }
```

<Frame caption="JsonEditor widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-json-editor.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=13c21684f62b3d4acacdceea2ed800e1" alt="JsonEditor widget on an action form" width="1300" height="810" data-path="images/actions/action-forms-widget-json-editor.png" />
</Frame>

### UserDropdown

Dropdown pre-filled with Forest users.

```javascript theme={null}
{ type: 'String', label: 'Assigned to', widget: 'UserDropdown' }
```

<Frame caption="UserDropdown widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-user-dropdown.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=b8b5f700df29a84cfd281749137e2221" alt="UserDropdown widget on an action form" width="1308" height="1212" data-path="images/actions/action-forms-widget-user-dropdown.png" />
</Frame>

### CurrencyInput

Number input with currency formatting.

```javascript theme={null}
{
  type: 'Number',
  label: 'Price',
  widget: 'CurrencyInput',
  options: { currency: 'USD' }
}
```

<Frame caption="CurrencyInput widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-currency-input.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=d97336bdfd708375a54318a9a8aa0943" alt="CurrencyInput widget on an action form" width="673" height="184" data-path="images/actions/action-forms-widget-currency-input.png" />
</Frame>

### RichText

Rich text editor with formatting options.

```javascript theme={null}
{ type: 'String', label: 'Content', widget: 'RichText' }
```

<Frame caption="RichText widget on an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-forms-widget-rich-text.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=d18f90f5d641f1272749f91b9324a5e7" alt="RichText widget on an action form" width="673" height="252" data-path="images/actions/action-forms-widget-rich-text.png" />
</Frame>

## Advanced patterns

### Multi-step forms

Create wizard-like forms by conditionally showing sections:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  form: [
    {
      type: 'Enum',
      label: 'Action type',
      id: 'actionType',
      enumValues: ['refund', 'replace', 'credit'],
    },
    // Refund fields
    {
      type: 'Number',
      label: 'Refund amount',
      if: context => context.formValues.actionType === 'refund',
    },
    // Replacement fields
    {
      type: 'Collection',
      label: 'Replacement product',
      collectionName: 'products',
      if: context => context.formValues.actionType === 'replace',
    },
    // Store credit fields
    {
      type: 'Number',
      label: 'Credit amount',
      if: context => context.formValues.actionType === 'credit',
    },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    {
      type: FieldType::ENUM,
      label: 'Action type',
      id: 'actionType',
      enum_values: ['refund', 'replace', 'credit']
    },
    # Refund fields
    {
      type: FieldType::NUMBER,
      label: 'Refund amount',
      if: ->(context) { context.form_values['actionType'] == 'refund' }
    },
    # Replacement fields
    {
      type: FieldType::COLLECTION,
      label: 'Replacement product',
      collection_name: 'products',
      if: ->(context) { context.form_values['actionType'] == 'replace' }
    },
    # Store credit fields
    {
      type: FieldType::NUMBER,
      label: 'Credit amount',
      if: ->(context) { context.form_values['actionType'] == 'credit' }
    }
  ]
  ```
</CodeGroup>

### Read-only fields for context

Show record data as read-only context:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  form: [
    {
      type: 'String',
      label: 'Customer name',
      isReadOnly: true,
      defaultValue: async context => {
        const order = await context.getRecord(['customer:name']);
        return order.customer.name;
      },
    },
    {
      type: 'Number',
      label: 'Refund amount',
      isRequired: true,
    },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    {
      type: FieldType::STRING,
      label: 'Customer name',
      is_read_only: true,
      default_value: ->(context) {
        order = context.get_record(['customer:name'])
        order['customer']['name']
      }
    },
    {
      type: FieldType::NUMBER,
      label: 'Refund amount',
      is_required: true
    }
  ]
  ```
</CodeGroup>

### Validation with required fields

Combine conditions for complex validation:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  {
    type: 'String',
    label: 'Manager approval',
    isRequired: context => {
      // Required if amount > 1000 and user is not admin
      return context.formValues.Amount > 1000 &&
             context.caller.role !== 'admin';
    },
  }
  ```

  ```ruby Ruby theme={null}
  {
    type: FieldType::STRING,
    label: 'Manager approval',
    is_required: ->(context) {
      # Required if amount > 1000 and user is not admin
      context.form_values['Amount'] > 1000 &&
        context.caller.role != 'admin'
    }
  }
  ```
</CodeGroup>

## Accessing form values

In the execute handler, access form values from the context:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  execute: async (context, resultBuilder) => {
    // Access by label
    const amount = context.formValues.Amount;

    // Access by id (if specified)
    const email = context.formValues['email'];

    // Access with spaces in label
    const firstName = context.formValues['First Name'];

    // Destructure multiple values
    const { Amount, Description } = context.formValues;
  }
  ```

  ```ruby Ruby theme={null}
  execute: ->(context, result_builder) {
    # Access by label
    amount = context.form_values['Amount']

    # Access by id (if specified)
    email = context.form_values['email']

    # Access with spaces in label
    first_name = context.form_values['First Name']

    # Multiple values
    amount = context.form_values['Amount']
    description = context.form_values['Description']
  }
  ```
</CodeGroup>

## Layout components

Organize your fields with layout components, separators, rows, HTML blocks, and multi-page forms. Useful when a form has many fields and you want to break it into manageable chunks.

### Common properties

| Property                                    | Required | Value                                           | Description                                  |
| ------------------------------------------- | -------- | ----------------------------------------------- | -------------------------------------------- |
| **type**                                    | Yes      | `"Layout"`                                      | Differentiates a layout element from a field |
| **component**                               | Yes      | `"Separator"`, `"Row"`, `"HtmlBlock"`, `"Page"` | The layout component to render               |
| **if** (Node.js) / **if\_condition** (Ruby) | No       | callable                                        | Only display if the function returns true    |

### Separator

A horizontal line between two form elements.

<CodeGroup>
  ```javascript Node.js theme={null}
  form: [
    { type: 'String', label: 'firstName' },
    { type: 'Layout', component: 'Separator' },
    { type: 'String', label: 'lastName' },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    { type: FieldType::STRING, label: 'firstName' },
    { type: FieldType::LAYOUT, component: 'Separator' },
    { type: FieldType::STRING, label: 'lastName' },
  ]
  ```
</CodeGroup>

<Frame caption="A separator between two fields">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-form-layout-separator.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=ac0aaa8f431ecb75901501ecec60e766" alt="Form with a horizontal separator between two fields" width="673" height="293" data-path="images/actions/action-form-layout-separator.png" />
</Frame>

### HTML block

Render arbitrary HTML content inside a form, useful for instructions, embedded content, or rich formatting.

| Property      | Required | Value                                 | Description            |
| ------------- | -------- | ------------------------------------- | ---------------------- |
| **component** | Yes      | `"HtmlBlock"`                         | Enables this component |
| **content**   | Yes      | string or callable returning a string | HTML content to render |

<CodeGroup>
  ```javascript Node.js theme={null}
  {
    type: 'Layout',
    component: 'HtmlBlock',
    content: ctx => `
      <div style="text-align:center;">
        <strong>Hi ${ctx.formValues.firstName} ${ctx.formValues.lastName}</strong>,
        here you can put <strong style="color: red;">all the HTML</strong> you want.
      </div>
    `,
  }
  ```

  ```ruby Ruby theme={null}
  {
    type: FieldType::LAYOUT,
    component: 'HtmlBlock',
    content: ->(context) {
      "<div style=\"text-align:center;\"><strong>Hi #{context.form_values['firstName']}</strong></div>"
    },
  }
  ```
</CodeGroup>

<Frame caption="An HTML block rendered inside an action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-form-layout-htmlblock.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=00d062272b08f3376e03196844e26fb1" alt="Action form with embedded HTML content" width="668" height="450" data-path="images/actions/action-form-layout-htmlblock.png" />
</Frame>

### Row

Display two fields side by side on the same line.

<Warning>
  A row is designed for **exactly two fields**. If `if` conditions hide one, the remaining field fills the line. If more than two are visible, only the first two render.
</Warning>

| Property      | Required | Value               | Description                                                                |
| ------------- | -------- | ------------------- | -------------------------------------------------------------------------- |
| **component** | Yes      | `"Row"`             | Enables this component                                                     |
| **fields**    | Yes      | array of two fields | The two fields to display side by side. No nested layout elements allowed. |

<CodeGroup>
  ```javascript Node.js theme={null}
  {
    type: 'Layout',
    component: 'Row',
    fields: [
      { label: 'gender', type: 'Enum', enumValues: ['M', 'F', 'other'] },
      {
        label: 'specify',
        type: 'String',
        if: ctx => ctx.formValues?.gender === 'other',
      },
    ],
  }
  ```

  ```ruby Ruby theme={null}
  {
    type: FieldType::LAYOUT,
    component: 'Row',
    fields: [
      { label: 'gender', type: FieldType::ENUM, enum_values: ['M', 'F', 'other'] },
      {
        label: 'specify',
        type: FieldType::STRING,
        if_condition: ->(context) { context.form_values['gender'] == 'other' },
      },
    ],
  }
  ```
</CodeGroup>

<Frame caption="Two fields displayed side by side in a row">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-form-layout-row.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=3b4ad02ce7101f231cd4855c4db0a0f7" alt="Two action form fields side by side" width="671" height="177" data-path="images/actions/action-form-layout-row.png" />
</Frame>

### Multi-page form

Break a long form into multiple pages, with next/previous navigation.

<Warning>
  When using pages, you **must have only** `Page` components at the root of your form, no mixing fields and pages at the root, no nesting pages inside pages.
</Warning>

| Property                                                               | Required | Value                               | Description                           |
| ---------------------------------------------------------------------- | -------- | ----------------------------------- | ------------------------------------- |
| **component**                                                          | Yes      | `"Page"`                            | Enables this component                |
| **elements**                                                           | Yes      | array of fields and layout elements | Fields and layouts shown on this page |
| **nextButtonLabel** (Node.js) / **next\_button\_label** (Ruby)         | No       | string                              | Label for the next button             |
| **previousButtonLabel** (Node.js) / **previous\_button\_label** (Ruby) | No       | string                              | Label for the previous button         |

<CodeGroup>
  ```javascript Node.js theme={null}
  form: [
    {
      type: 'Layout',
      component: 'Page',
      nextButtonLabel: 'Go to address',
      elements: [
        { type: 'String', id: 'Firstname', label: 'First name' },
        { type: 'String', id: 'Lastname', label: 'Last name' },
        { type: 'Layout', component: 'Separator' },
        { type: 'Date', id: 'Birthdate', label: 'Birth date' },
      ],
    },
    {
      type: 'Layout',
      component: 'Page',
      previousButtonLabel: 'Go back to identity',
      elements: [
        {
          type: 'Layout',
          component: 'Row',
          fields: [
            { type: 'Number', id: 'StreetNumber', label: 'Street number' },
            { type: 'String', id: 'StreetName', label: 'Street name' },
          ],
        },
        { type: 'String', id: 'PostalCode', label: 'Postal code' },
        { type: 'String', id: 'City', label: 'City' },
        { type: 'String', id: 'Country', label: 'Country' },
      ],
    },
  ]
  ```

  ```ruby Ruby theme={null}
  form: [
    {
      type: FieldType::LAYOUT,
      component: 'Page',
      next_button_label: 'Go to address',
      elements: [
        { type: FieldType::STRING, id: 'Firstname', label: 'First name' },
        { type: FieldType::STRING, id: 'Lastname', label: 'Last name' },
        { type: FieldType::LAYOUT, component: 'Separator' },
        { type: FieldType::DATE, id: 'Birthdate', label: 'Birth date' },
      ],
    },
    {
      type: FieldType::LAYOUT,
      component: 'Page',
      previous_button_label: 'Go back to identity',
      elements: [
        { type: FieldType::STRING, id: 'PostalCode', label: 'Postal code' },
        { type: FieldType::STRING, id: 'City', label: 'City' },
        { type: FieldType::STRING, id: 'Country', label: 'Country' },
      ],
    },
  ]
  ```
</CodeGroup>

<Frame caption="First page of a multi-page action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-form-layout-pages-1.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=0d29c6e44398f2376f662a2e6bcc9396" alt="Page 1 of a multi-page action form" width="656" height="717" data-path="images/actions/action-form-layout-pages-1.png" />
</Frame>

<Frame caption="Second page of a multi-page action form">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/action-form-layout-pages-2.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=cfc2e9df595f8aa1bae7f08d78acfba3" alt="Page 2 of a multi-page action form" width="657" height="713" data-path="images/actions/action-form-layout-pages-2.png" />
</Frame>

<Note>
  If every element on a page is hidden by `if` conditions, the page is automatically removed. To prevent this, add an unconditional `HtmlBlock` explaining why the page is empty.
</Note>
