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

# Ruby Agent API Reference

> Complete API reference for the Forest Ruby Agent

Complete API reference for Forest Ruby agent packages.

## Agent Setup

### Creating an Agent

The Ruby agent is designed for Rails applications and automatically introspects your data models.

```ruby theme={null}
# Gemfile
gem 'forest_admin_agent'
gem 'forest_admin_rails'
gem 'forest_admin_datasource_toolkit'
gem 'forest_admin_datasource_customizer'
gem 'forest_admin_datasource_active_record' # For ActiveRecord
# or
gem 'forest_admin_datasource_mongoid' # For Mongoid
```

**Installation:**

```bash theme={null}
bundle install
rails generate forest_admin_rails:install
```

The generator creates two files:

* `config/initializers/forest_admin_rails.rb`, secrets and configuration
* `app/lib/forest_admin_rails/create_agent.rb`, datasource setup and collection customizations

**Configuration** (`config/initializers/forest_admin_rails.rb`):

```ruby theme={null}
ForestAdminRails.configure do |config|
  config.auth_secret = ENV.fetch('FOREST_AUTH_SECRET')
  config.env_secret = ENV.fetch('FOREST_ENV_SECRET')
end
```

**Datasource setup and customizations** (`app/lib/forest_admin_rails/create_agent.rb`):

```ruby theme={null}
module ForestAdminRails
  class CreateAgent
    def self.setup!
      database_configuration = Rails.configuration.database_configuration
      datasource = ForestAdminDatasourceActiveRecord::Datasource.new(database_configuration[Rails.env])

      @create_agent = ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(datasource)
      customize
      @create_agent.build
    end

    def self.customize
      # All your collection customizations go here, see below.
    end
  end
end
```

**Configuration Options** (passed to `ForestAdminRails.configure`):

#### Required

| Option | Type | Description |
| - | - | - |
| `auth_secret` | String | Your `FOREST_AUTH_SECRET`. Any long random string. |
| `env_secret` | String | Your `FOREST_ENV_SECRET`, a 64-character hexadecimal string retrieved from Forest Admin. |

Outside production an invalid secret only logs a warning and skips the schema sync; in production it raises.

#### Connection and routing

| Option | Type | Default | Description |
| - | - | - | - |
| `prefix` | String | `nil` | Path the engine is mounted on. Read by `rails generate forest_admin_rails:install` when it writes the `mount` line in `config/routes.rb`, so changing it afterwards means editing that route yourself. |
| `is_production` | Boolean | `Rails.env.production?` | Production mode. The schema is read from `schema_path` instead of being rebuilt, and schema-sync failures raise instead of being logged as warnings. |

#### Schema

| Option | Type | Default | Description |
| - | - | - | - |
| `schema_path` | String | `.forestadmin-schema.json` in the working directory | Path of the schema file. Written on every boot outside production, required and read at boot in production. |
| `append_schema_path` | String | `nil` | Path of an extra JSON schema whose `collections` are appended to the generated schema before it is sent. Raises at boot when the file cannot be read or parsed. |
| `skip_schema_update` | Boolean | `false` | Never send the schema to Forest. Logs a warning at boot. |

#### Logging and errors

| Option | Type | Default | Description |
| - | - | - | - |
| `logger` | Lambda | `nil` | Custom logger, called as `->(level, message)`. Falls back to a `MonoLogger` on `$stdout`. |
| `logger_level` | String | `'info'` | Minimum level of the default logger: `'Debug'`, `'Info'`, `'Warn'`, `'Error'`, `'Fatal'` or `'Unknown'`. |
| `customize_error_message` | Lambda | `nil` | Rewrite the message sent to the UI for unexpected errors, called as `->(error)`. Return `nil` to keep the default message. |
| `debug` | Boolean | `true` | Disables TLS certificate verification on the OIDC authentication client and on the RPC datasource client. |

<Warning>
  `debug` defaults to `true`, which turns **off** TLS certificate verification for
  authentication and RPC calls. Set `config.debug = false` in any environment
  exposed to an untrusted network.
</Warning>

#### Permissions and caching

| Option | Type | Default | Description |
| - | - | - | - |
| `instant_cache_refresh` | Boolean | `Rails.env.production?` | Keep permissions in sync through a Server-Sent Events connection to Forest. Set it to `false` when your infrastructure does not support long-lived connections; the agent then relies on `permission_expiration` alone. |
| `permission_expiration` | Integer | `900` | Lifetime in seconds of the permission and IP-whitelist caches. |
| `cache_dir` | String | `tmp/cache/forest_admin` | Directory holding the on-disk caches (authentication issuer, permissions, IP whitelist). |
| `skip_relation_read_permissions` | Boolean | `false` | Serve columns of collections the caller has no `read` permission on when they are reached through a relation path. The permission check on the queried collection itself still runs. Logs a warning at boot. |
| `max_records_for_approval` | Integer | `500` | Maximum number of records a "select all" approval-required action may target. Capped at `500`; any value that is not a positive integer falls back to the default. |

<Warning>
  `skip_relation_read_permissions` widens what your users can read. The agent logs
  a warning at boot when it is enabled.
</Warning>

#### Performance

| Option | Type | Default | Description |
| - | - | - | - |
| `disable_route_cache` | Boolean | `false` | Recompute the route table on every request instead of caching it at boot. |
| `rpc_max_polling_threads` | Integer | `nil` | Size of the polling thread pool of the RPC datasource. Ignored when the value is not positive or when `forest_admin_datasource_rpc` is not loaded. |
| `limit_export_size` | Integer | `nil` | Maximum number of records a CSV export returns. Unlimited when unset. |

#### Integrations

| Option | Type | Default | Description |
| - | - | - | - |
| `workflow_executor_url` | String | `nil` | Base URL of the [Forest Runtime](/product/process/workflows/forest-runtime) instance workflow requests are proxied to. The workflow routes return `404` while it is unset. |
| `audit_trail` | Hash | `nil` | Record every create, update and delete into a SQL database. See [Audit trail](/get-started/control/audit-trail). |

## Customizing Collections

### agent.customize\_collection(name, \&block)

Customize a specific collection with the provided block.

```ruby theme={null}
# Inside ForestAdminRails::CreateAgent.customize
@create_agent.customize_collection('User') do |collection|
  collection.add_action('Send email', {
    scope: 'Single',
    execute: ->(context, result_builder) {
      # Action logic
      result_builder.success('Email sent!')
    }
  })
end
```

**Parameters:**

| Parameter | Type | Description |
| - | - | - |
| `name` | String | Collection name |
| `block` | Block | Customization block |

***

## Datasources

### agent.add\_datasource(datasource, options = {})

Add a datasource to the agent. Called inside `ForestAdminRails::CreateAgent.setup!`.

```ruby theme={null}
# Inside ForestAdminRails::CreateAgent.setup!
@create_agent = ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(
  ForestAdminDatasourceActiveRecord::Datasource.new(database_configuration[Rails.env]),
  exclude: ['internal_logs']
)
```

**Options:**

| Option | Type | Description |
| - | - | - |
| `include` | Array\<String> | Collections to include |
| `exclude` | Array\<String> | Collections to exclude |
| `rename` | Hash | Rename collections |

**Example with Mongoid:**

```ruby theme={null}
# Inside ForestAdminRails::CreateAgent.setup!
@create_agent = ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(
  ForestAdminDatasourceMongoid::Datasource.new,
  rename: { 'old_name' => 'new_name' }
)
```

***

## Actions

### collection.add\_action(name, definition)

Add an action to the collection.

```ruby theme={null}
collection.add_action('Send email', {
  scope: 'Single',
  execute: ->(context, result_builder) {
    user = context.get_record(['id', 'email'])
    UserMailer.notification(user['email']).deliver_later
    result_builder.success('Email sent!')
  }
})
```

**Definition Properties:**

| Property | Type | Description |
| - | - | - |
| `scope` | Symbol | Action scope: `:Single`, `:Bulk`, or `:Global` |
| `execute` | Proc | Action execution handler |
| `form` | Array | Dynamic form configuration |
| `description` | String | Action description |
| `generate_file` | Boolean | Whether action returns a file |
| `submit_button_label` | String | Custom button text |

**Execute Block:**

```ruby theme={null}
execute: ->(context, result_builder) {
  # Action logic
}
```

**ActionContext Methods:**

* `context.collection` - Collection instance
* `context.filter` - Filter for selected records
* `context.caller` - User who triggered the action
* `context.form_values` - Form values submitted
* `context.get_records(fields)` - Get multiple records
* `context.get_record(fields)` - Get single record (Single scope)
* `context.get_record_ids` - Get IDs of selected records
* `context.has_field_changed(field_name)` - Check if form field changed

**ResultBuilder Methods:**

* `result_builder.success(message, options = {})` - Success response
  * `options[:html]` - Custom HTML to display
  * `options[:invalidated]` - Array of collection names to refresh
* `result_builder.error(message, options = {})` - Error response
* `result_builder.webhook(url, method, headers, body)` - Trigger webhook
* `result_builder.file(stream, filename, mime_type)` - Return file download
* `result_builder.redirect_to(path)` - Redirect to URL
* `result_builder.set_header(name, value)` - Add HTTP header

**Example - Action with Form:**

```ruby theme={null}
collection.add_action('Send notification', {
  scope: 'Bulk',
  form: [
    {
      label: 'Message',
      type: 'String',
      is_required: true
    },
    {
      label: 'Channel',
      type: 'Enum',
      enum_values: ['email', 'sms', 'push'],
      is_required: true
    }
  ],
  execute: ->(context, result_builder) {
    message = context.form_values['message']
    channel = context.form_values['channel']
    users = context.get_records(['email'])

    users.each do |user|
      NotificationService.send(user['email'], message, channel)
    end

    result_builder.success("Sent #{channel} to #{users.length} users")
  }
})
```

**Example - File Generation:**

```ruby theme={null}
collection.add_action('Export to CSV', {
  scope: 'Bulk',
  generate_file: true,
  execute: ->(context, result_builder) {
    records = context.get_records(['name', 'email'])
    csv_stream = CsvGenerator.generate(records)

    result_builder.file(csv_stream, 'export.csv', 'text/csv')
  }
})
```

***

## Fields

### collection.add\_field(name, definition)

Add a computed field to the collection.

```ruby theme={null}
collection.add_field('full_name', {
  column_type: 'String',
  dependencies: ['first_name', 'last_name'],
  get_values: ->(records) {
    records.map { |r| "#{r['first_name']} #{r['last_name']}" }
  }
})
```

**Definition Properties:**

| Property | Type | Required | Description |
| - | - | - | - |
| `column_type` | String | Yes | Field data type |
| `dependencies` | Array | Yes | Fields needed for computation |
| `get_values` | Proc | Yes | Value computation function |
| `default_value` | Any | No | Default value |
| `enum_values` | Array | No | Enum options (if type is Enum) |

**Column Types:**

* `'String'` - Text
* `'Number'` - Numeric value
* `'Boolean'` - True/false
* `'Date'` - Date with time
* `'Dateonly'` - Date without time
* `'Enum'` - Enumeration
* `'Json'` - JSON object
* `'Uuid'` - UUID

**Example - Async Computed Field:**

```ruby theme={null}
collection.add_field('revenue_this_year', {
  column_type: 'Number',
  dependencies: ['id'],
  get_values: ->(records) {
    ids = records.map { |r| r['id'] }
    RevenueCalculator.fetch_for_ids(ids)
  }
})
```

***

### collection.import\_field(name, options)

Import a field from a related collection.

```ruby theme={null}
# Import author's name into books collection
collection.import_field('author_name', {
  path: 'author:full_name',
  readonly: true
})
```

**Options:**

| Option | Type | Description |
| - | - | - |
| `path` | String | Relationship path (e.g., 'author:full\_name') |
| `readonly` | Boolean | Whether field is read-only |

***

### collection.rename\_field(current\_name, new\_name)

Rename a field in the exported schema.

```ruby theme={null}
collection.rename_field('created_at', 'createdAt')
```

***

### collection.remove\_field(\*names)

Remove fields from the exported schema.

```ruby theme={null}
collection.remove_field('password', 'internal_notes', 'debug_data')
```

***

### collection.replace\_field\_writing(name, definition)

Replace the write behavior of a field.

```ruby theme={null}
# Write full_name as first_name + last_name
collection.replace_field_writing('full_name', ->(full_name) {
  parts = full_name.split(' ', 2)
  { 'first_name' => parts[0], 'last_name' => parts[1] }
})
```

***

## Segments

### collection.add\_segment(name, definition)

Add a segment (saved filter) to the collection.

```ruby theme={null}
collection.add_segment('Premium users', {
  field: 'plan',
  operator: 'Equal',
  value: 'premium'
})
```

**Example - Static Segment:**

```ruby theme={null}
collection.add_segment('Active users', {
  field: 'status',
  operator: 'Equal',
  value: 'active'
})
```

**Example - Dynamic Segment:**

```ruby theme={null}
collection.add_segment('Active this month', ->(context) {
  start_of_month = Date.today.beginning_of_month

  {
    field: 'last_active_at',
    operator: 'After',
    value: start_of_month
  }
})
```

**Example - Complex Segment:**

```ruby theme={null}
collection.add_segment('VIP customers', ->(context) {
  {
    aggregator: 'And',
    conditions: [
      { field: 'status', operator: 'Equal', value: 'active' },
      { field: 'lifetime_value', operator: 'GreaterThan', value: 10000 }
    ]
  }
})
```

***

## Relationships

### collection.add\_many\_to\_one\_relation(name, foreign\_collection, options)

Add a many-to-one relationship.

```ruby theme={null}
# books.author_id → persons.id
collection.add_many_to_one_relation('author', 'Person', {
  foreign_key: 'author_id'
})
```

**Options:**

| Option | Type | Description |
| - | - | - |
| `foreign_key` | String | Foreign key field name |
| `foreign_key_target` | String | Target field (default: 'id') |

***

### collection.add\_one\_to\_many\_relation(name, foreign\_collection, options)

Add a one-to-many relationship.

```ruby theme={null}
# persons.id ← books.author_id
collection.add_one_to_many_relation('written_books', 'Book', {
  origin_key: 'author_id'
})
```

**Options:**

| Option | Type | Description |
| - | - | - |
| `origin_key` | String | Foreign key in related collection |
| `origin_key_target` | String | Target field (default: 'id') |

***

### collection.add\_one\_to\_one\_relation(name, foreign\_collection, options)

Add a one-to-one relationship.

```ruby theme={null}
# persons.id ← profiles.person_id (unique)
collection.add_one_to_one_relation('profile', 'Profile', {
  origin_key: 'person_id'
})
```

***

### collection.add\_many\_to\_many\_relation(name, foreign\_collection, through\_collection, options)

Add a many-to-many relationship.

```ruby theme={null}
# students ↔ student_courses ↔ courses
collection.add_many_to_many_relation('enrolled_courses', 'Course', 'StudentCourse', {
  origin_key: 'student_id',
  foreign_key: 'course_id'
})
```

**Options:**

| Option | Type | Description |
| - | - | - |
| `origin_key` | String | Foreign key to origin collection |
| `foreign_key` | String | Foreign key to target collection |
| `origin_key_target` | String | Origin target field |
| `foreign_key_target` | String | Foreign target field |

***

## Hooks

### collection.add\_hook(position, type, handler)

Add a hook to execute code before or after operations.

```ruby theme={null}
collection.add_hook('Before', 'Create', ->(context) {
  # Validate data before creation
  if context.data['email'].nil?
    raise 'Email is required'
  end
})
```

**Hook Types:**

* `'List'` - Before/after listing records
* `'Create'` - Before/after creating records
* `'Update'` - Before/after updating records
* `'Delete'` - Before/after deleting records
* `'Aggregate'` - Before/after aggregating data

**Example - Before Hook:**

```ruby theme={null}
collection.add_hook('Before', 'Create', ->(context) {
  # Set default values
  context.data['status'] ||= 'active'
  context.data['created_by'] = context.caller.id
})
```

**Example - After Hook:**

```ruby theme={null}
collection.add_hook('After', 'Update', ->(context) {
  # Send notification after update
  records = context.collection.list(context.filter, ['email'])
  records.each do |record|
    NotificationService.send_update_email(record['email'])
  end
})
```

***

## Charts

### collection.add\_chart(name, definition)

Add a chart to the collection.

```ruby theme={null}
collection.add_chart('total_revenue', ->(context, result_builder) {
  total = Order.sum(:total)
  result_builder.value(total)
})
```

**Chart Types:**

**Value Chart:**

```ruby theme={null}
collection.add_chart('user_count', ->(context, result_builder) {
  count = User.count
  result_builder.value(count)
})
```

**Distribution Chart:**

```ruby theme={null}
collection.add_chart('users_by_plan', ->(context, result_builder) {
  distribution = User.group(:plan).count
  result_builder.distribution(distribution)
})
```

**Time-based Chart:**

```ruby theme={null}
collection.add_chart('signups_over_time', ->(context, result_builder) {
  data = User.group_by_day(:created_at).count
  result_builder.time_based('Day', data)
})
```

**Percentage Chart:**

```ruby theme={null}
collection.add_chart('completion_rate', ->(context, result_builder) {
  completed = Task.where(status: 'completed').count
  total = Task.count
  rate = (completed.to_f / total * 100).round(2)
  result_builder.percentage(rate)
})
```

**Objective Chart:**

```ruby theme={null}
collection.add_chart('sales_goal', ->(context, result_builder) {
  current = Order.sum(:total)
  target = 100_000
  result_builder.objective(current, target)
})
```

**Leaderboard Chart:**

```ruby theme={null}
collection.add_chart('top_sellers', ->(context, result_builder) {
  top = User.joins(:orders)
    .group('users.name')
    .sum('orders.total')
  result_builder.leaderboard(top)
})
```

***

## Search & Sorting

### collection.replace\_search(definition)

Replace the default search behavior.

```ruby theme={null}
collection.replace_search(->(search_string) {
  {
    aggregator: 'Or',
    conditions: [
      { field: 'first_name', operator: 'Contains', value: search_string },
      { field: 'last_name', operator: 'Contains', value: search_string },
      { field: 'email', operator: 'Contains', value: search_string }
    ]
  }
})
```

***

### collection.disable\_search

Disable search functionality on the collection.

```ruby theme={null}
collection.disable_search
```

***

### collection.replace\_field\_sorting(name, equivalent\_sort)

Replace sorting implementation for a field.

```ruby theme={null}
collection.replace_field_sorting('full_name', [
  { field: 'last_name', ascending: true },
  { field: 'first_name', ascending: true }
])
```

***

## Form Field Types

Action forms support various field types.

```ruby theme={null}
collection.add_action('Example', {
  scope: 'Single',
  form: [
    {
      label: 'User Name',
      type: 'String',
      is_required: true,
      description: 'Enter the user name'
    },
    {
      label: 'Age',
      type: 'Number',
      default_value: 18
    },
    {
      label: 'Is Active',
      type: 'Boolean',
      default_value: true
    },
    {
      label: 'Birth Date',
      type: 'Date'
    },
    {
      label: 'Status',
      type: 'Enum',
      enum_values: ['pending', 'approved', 'rejected']
    }
  ],
  execute: ->(context, result_builder) {
    values = context.form_values
    # ... action logic
    result_builder.success
  }
})
```

**Available Types:**

* `'String'` - Text input
* `'Number'` - Numeric input
* `'Boolean'` - Checkbox
* `'Date'` - Date picker
* `'Dateonly'` - Date without time
* `'Enum'` - Single selection
* `'EnumList'` - Multiple selection
* `'File'` - File upload
* `'Json'` - JSON editor
* `'Collection'` - Record picker

**Collection Field:**

```ruby theme={null}
{
  label: 'Assign to User',
  type: 'Collection',
  collection_name: 'User'
}
```

**Conditional Fields:**

```ruby theme={null}
form: [
  {
    label: 'Notification Type',
    type: 'Enum',
    enum_values: ['email', 'sms', 'push']
  },
  {
    label: 'Email Address',
    type: 'String',
    if: ->(context) { context.form_values['notification_type'] == 'email' }
  }
]
```

***

## Complete Example

```ruby theme={null}
# config/initializers/forest_admin_rails.rb
ForestAdminRails.configure do |config|
  config.auth_secret = ENV.fetch('FOREST_AUTH_SECRET')
  config.env_secret = ENV.fetch('FOREST_ENV_SECRET')
end
```

```ruby theme={null}
# app/lib/forest_admin_rails/create_agent.rb
module ForestAdminRails
  class CreateAgent
    def self.setup!
      database_configuration = Rails.configuration.database_configuration
      datasource = ForestAdminDatasourceActiveRecord::Datasource.new(database_configuration[Rails.env])

      @create_agent = ForestAdminAgent::Builder::AgentFactory.instance.add_datasource(datasource)
      customize
      @create_agent.build
    end

    def self.customize
      # Customize User collection
      @create_agent.customize_collection('User') do |collection|
        # Add computed field
        collection.add_field('full_name', {
          column_type: 'String',
          dependencies: ['first_name', 'last_name'],
          get_values: ->(records) {
            records.map { |r| "#{r['first_name']} #{r['last_name']}" }
          }
        })

        # Add segment
        collection.add_segment('Active users', {
          field: 'status',
          operator: 'Equal',
          value: 'active'
        })

        # Add action
        collection.add_action('Send promotional email', {
          scope: 'Bulk',
          form: [
            {
              label: 'Campaign',
              type: 'Enum',
              enum_values: ['summer', 'winter', 'black_friday']
            },
            {
              label: 'Discount',
              type: 'Number',
              is_required: true
            }
          ],
          execute: ->(context, result_builder) {
            campaign = context.form_values['campaign']
            discount = context.form_values['discount']
            users = context.get_records(['email'])

            users.each do |user|
              PromotionalMailer.campaign_email(
                user['email'],
                campaign,
                discount
              ).deliver_later
            end

            result_builder.success("Email sent to #{users.length} users")
          }
        })

        # Add hook
        collection.add_hook('Before', 'Create', ->(context) {
          context.data['status'] ||= 'active'
          context.data['created_by'] = context.caller.id
        })

        # Add chart
        collection.add_chart('user_count', ->(context, result_builder) {
          count = User.count
          result_builder.value(count)
        })
      end
    end
  end
end
```

***

## Available Operators

**Comparison:**

* `'Equal'`, `'NotEqual'`
* `'GreaterThan'`, `'LessThan'`
* `'In'`, `'NotIn'`
* `'Present'`, `'Blank'`

**String:**

* `'Contains'`, `'NotContains'`
* `'StartsWith'`, `'EndsWith'`
* `'Like'`, `'ILike'` (case-insensitive)

**Date:**

* `'Before'`, `'After'`
* `'Today'`, `'Yesterday'`
* `'PreviousWeek'`, `'PreviousMonth'`, `'PreviousQuarter'`, `'PreviousYear'`
* `'Past'`, `'Future'`

***

## Context Objects

### Caller

User information available in all contexts:

```ruby theme={null}
context.caller.id          # User ID
context.caller.email       # User email
context.caller.first_name  # User first name
context.caller.last_name   # User last name
context.caller.team        # User team
context.caller.role        # User role
context.caller.timezone    # User timezone
```
