> ## 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.

# Agent API Reference

> Complete API reference for the Forest Node.js Agent

Complete API reference for `@forestadmin/agent` (Node.js/TypeScript).

## Agent Class

### createAgent(options)

Create and configure a Forest agent.

```typescript theme={null}
import { createAgent } from '@forestadmin/agent';

const agent = createAgent(options: AgentOptions): Agent;
```

**Parameters:**

| Option                                                              | Type     | Required | Description                                                               |
| ------------------------------------------------------------------- | -------- | -------- | ------------------------------------------------------------------------- |
| `authSecret`                                                        | string   | Yes      | Your FOREST\_AUTH\_SECRET                                                 |
| `envSecret`                                                         | string   | Yes      | Your FOREST\_ENV\_SECRET                                                  |
| `isProduction`                                                      | boolean  | No       | Enable production mode                                                    |
| `logger`                                                            | function | No       | Custom logger function                                                    |
| `loggerLevel`                                                       | string   | No       | Log level: 'Debug', 'Info', 'Warn', 'Error'                               |
| `prefix`                                                            | string   | No       | API prefix (default: '/forest')                                           |
| `schemaPath`                                                        | string   | No       | Path to .forestadmin-schema.json                                          |
| `typingsPath`                                                       | string   | No       | Path to the generated TypeScript typings file (written when set)          |
| <code style={{whiteSpace: 'nowrap'}}>useUnsafeActionEndpoint</code> | boolean  | No       | Drop the positional index from Smart Action routes.<br />Default `false`. |

**Example:**

```typescript theme={null}
const agent = createAgent({
  authSecret: process.env.FOREST_AUTH_SECRET,
  envSecret: process.env.FOREST_ENV_SECRET,
  isProduction: process.env.NODE_ENV === 'production',
});
```

<Warning>
  Only enable `useUnsafeActionEndpoint` if you specifically need index-free Smart
  Action routes.<br />
  It removes the positional index
  (`/_actions/{collection}/{slug}` instead of `/_actions/{collection}/{index}/{slug}`),
  which can make two action names collide on the same route.<br />
  When enabled, the agent refuses to start if two actions on the same collection
  resolve to the same slug.
</Warning>

***

### agent.addDataSource(factory, options?)

Add a datasource to the agent.

```typescript theme={null}
agent.addDataSource(
  factory: DataSourceFactory,
  options?: DataSourceOptions
): Agent;
```

**Parameters:**

| Option            | Type                    | Description                 |
| ----------------- | ----------------------- | --------------------------- |
| `factory`         | DataSourceFactory       | Datasource factory function |
| `options.include` | string\[]               | Collections to include      |
| `options.exclude` | string\[]               | Collections to exclude      |
| `options.rename`  | Record\<string, string> | Rename collections          |

**Example:**

```typescript theme={null}
import { createSqlDataSource } from '@forestadmin/datasource-sql';

agent.addDataSource(
  createSqlDataSource(process.env.DATABASE_URL),
  { exclude: ['internal_logs'] }
);
```

***

### agent.customizeCollection(name, callback)

Customize a specific collection with the provided callback.

```typescript theme={null}
agent.customizeCollection(
  name: string,
  callback: (collection: CollectionCustomizer) => void
): Agent;
```

**Example:**

```typescript theme={null}
agent.customizeCollection('users', collection => {
  collection.addAction('Send email', {
    scope: 'Single',
    execute: async (context, resultBuilder) => {
      // Action logic
      return resultBuilder.success('Email sent!');
    },
  });
});
```

***

### agent.addChart(name, definition)

Create a datasource-level API chart.

```typescript theme={null}
agent.addChart(
  name: string,
  definition: DataSourceChartDefinition
): Agent;
```

**Example:**

```typescript theme={null}
agent.addChart('overview', (context, resultBuilder) => {
  return resultBuilder.distribution({
    'Active': 150,
    'Inactive': 50,
  });
});
```

***

### agent.removeCollection(...names)

Remove collections from the exported schema (they remain usable within the agent).

```typescript theme={null}
agent.removeCollection(...names: string[]): Agent;
```

**Example:**

```typescript theme={null}
agent.removeCollection('internalLogs', 'debugData');
```

***

### agent.use(plugin, options?)

Load a plugin across all collections.

```typescript theme={null}
agent.use<Options>(
  plugin: Plugin<Options>,
  options?: Options
): Agent;
```

**Example:**

```typescript theme={null}
import advancedExportPlugin from '@forestadmin/plugin-export-advanced';

agent.use(advancedExportPlugin, { format: 'xlsx' });
```

***

### agent.start()

Start the agent and connect to Forest servers.

```typescript theme={null}
await agent.start(): Promise<void>;
```

**Example:**

```typescript theme={null}
await agent.start();
console.log('Agent started successfully');
```

***

### agent.stop()

Stop the agent and close all connections.

```typescript theme={null}
await agent.stop(): Promise<void>;
```

***

### agent.restart()

Reconstruct routing and remount routes at runtime. Called when customizations are refreshed externally (e.g. in cloud environments).

```typescript theme={null}
await agent.restart(): Promise<void>;
```

***

### agent.addAi(provider)

Route Forest's AI features (natural language search, actions, etc.) through your own agent so your data never leaves your infrastructure. Pass a provider built with `createAiProvider` (from `@forestadmin/ai-proxy`); `openai` and `anthropic` are supported. See [Self-hosted AI](/product/process/advanced-concepts/self-hosted-ai) for the full guide.

```typescript theme={null}
agent.addAi(provider: AiProviderDefinition): Agent;
```

Can only be called once, calling it a second time throws an error.

**Example:**

```typescript theme={null}
import { createAgent } from '@forestadmin/agent';
import { createAiProvider } from '@forestadmin/ai-proxy';

const agent = createAgent({
  authSecret: process.env.FOREST_AUTH_SECRET,
  envSecret: process.env.FOREST_ENV_SECRET,
})
  .addAi(
    createAiProvider({
      name: 'my-assistant',
      provider: 'anthropic',
      apiKey: process.env.ANTHROPIC_API_KEY,
      model: 'claude-sonnet-4-5',
    }),
  );
```

***

### agent.mountAiMcpServer(options?)

Enable a Model Context Protocol (MCP) server on the agent, allowing AI assistants to interact with your data through standardized tools.

```typescript theme={null}
agent.mountAiMcpServer(options?: {
  enabledTools?: ToolName[];
}): Agent;
```

**Parameters:**

| Option         | Type         | Description                                                  |
| -------------- | ------------ | ------------------------------------------------------------ |
| `enabledTools` | `ToolName[]` | Restrict which MCP tools are exposed. Defaults to all tools. |

**Available tool names:** `'describeCollection'`, `'list'`, `'listRelated'`, `'create'`, `'update'`, `'delete'`, `'associate'`, `'dissociate'`, `'getActionForm'`, `'executeAction'`

**Example:**

```typescript theme={null}
agent.mountAiMcpServer({
  enabledTools: ['describeCollection', 'list', 'listRelated'],
});
```

The MCP server exposes HTTP endpoints for OAuth and protocol communication:

| Endpoint                                        | Purpose                                            |
| ----------------------------------------------- | -------------------------------------------------- |
| `POST /mcp`                                     | Main MCP protocol endpoint (Bearer token required) |
| `POST /oauth/authorize`                         | OAuth authorization                                |
| `POST /oauth/token`                             | Token exchange                                     |
| `GET /.well-known/oauth-protected-resource/mcp` | OAuth discovery                                    |

You can also configure enabled tools via the `FOREST_MCP_ENABLED_TOOLS` environment variable (comma-separated tool names), or set the server port with `MCP_SERVER_PORT` (default: `3931`).

***

### agent.updateTypesOnFileSystem(typingsPath, typingsMaxDepth)

Update the TypeScript typings file generated from your datasources.

```typescript theme={null}
await agent.updateTypesOnFileSystem(
  typingsPath: string,
  typingsMaxDepth: number
): Promise<void>;
```

**Example:**

```typescript theme={null}
await agent.updateTypesOnFileSystem('./typings.ts', 5);
```

***

### agent.generateSchemaOnly()

Build the schema (`.forestadmin-schema.json`) and the TypeScript typings and write them to disk **without** starting the agent or sending the schema to Forest. Useful for generating the schema at build time in a CI/CD pipeline, see [Generate the schema at build time](/get-started/deploy#generate-the-schema-at-build-time). Available since `@forestadmin/agent` 1.83.0.

```typescript theme={null}
await agent.generateSchemaOnly(): Promise<void>;
```

It writes to the `schemaPath` (and `typingsPath`, when set) you configured in `createAgent`. Unlike `agent.start()`, it always rebuilds the schema, even when `isProduction` is `true`, and it never contacts Forest, with one exception: if you enable experimental no-code customizations, it still fetches their configuration from the Forest API, so connectivity is required in that case.

**Example:**

```typescript theme={null}
import { createAgent } from '@forestadmin/agent';
import { createSqlDataSource } from '@forestadmin/datasource-sql';

const agent = createAgent({
  authSecret: process.env.FOREST_AUTH_SECRET,
  envSecret: process.env.FOREST_ENV_SECRET,
  isProduction: true,
  schemaPath: '.forestadmin-schema.json',
  typingsPath: './typings.ts', // optional
}).addDataSource(createSqlDataSource(process.env.DATABASE_URL));

await agent.generateSchemaOnly();
```

<Note>
  This does not close your data source connections. In a one-shot script, close your data source (the SQL/Sequelize/Mongo client you passed in) or call `process.exit()` once it resolves, otherwise an open connection pool can keep the process alive.
</Note>

***

## Collection Customizer

Methods available when customizing a collection through `agent.customizeCollection()`.

## Actions

### collection.addAction(name, definition)

Add an action to the collection.

```typescript theme={null}
collection.addAction(
  name: string,
  definition: ActionDefinition
): CollectionCustomizer;
```

**Definition Properties:**

| Property            | Type                           | Description                   |
| ------------------- | ------------------------------ | ----------------------------- |
| `scope`             | 'Single' \| 'Bulk' \| 'Global' | Action scope                  |
| `execute`           | function                       | Action execution handler      |
| `form`              | FormElement\[]                 | Dynamic form configuration    |
| `description`       | string                         | Action description            |
| `generateFile`      | boolean                        | Whether action returns a file |
| `submitButtonLabel` | string                         | Custom button text            |

**Execute Function:**

```typescript theme={null}
execute: (
  context: ActionContext,
  resultBuilder: ResultBuilder
) => Promise<ActionResult>
```

**ActionContext Properties:**

* `context.collection` - Collection instance
* `context.filter` - Filter for selected records
* `context.caller` - User who triggered the action
* `context.formValues` - Form values submitted

**ActionContext Methods:**

* `context.getRecords(fields)` - Get multiple records (Bulk/Single scope)
* `context.getRecordIds()` - Get IDs of selected records
* `context.getCompositeRecordIds()` - Get composite IDs of selected records
* `context.hasFieldChanged(fieldName)` - Check if form field changed
* `context.getRecord(fields)` - Get single record (Single scope only)
* `context.getRecordId()` - Get single record ID (Single scope only)
* `context.getCompositeRecordId()` - Get composite ID (Single scope only)
* `context.getField(fieldName)` - Get single field value (Single scope only)

**ResultBuilder Methods:**

* `resultBuilder.success(message?, options?)` - Success response
  * `options.html` - Custom HTML to display
  * `options.invalidated` - Array of collection names to refresh
* `resultBuilder.error(message?, options?)` - Error response
  * `options.html` - Custom HTML to display
* `resultBuilder.webhook(url, method, headers, body)` - Trigger webhook
* `resultBuilder.file(stream, filename, mimeType)` - Return file download
* `resultBuilder.redirectTo(path)` - Redirect to URL
* `resultBuilder.setHeader(name, value)` - Add HTTP header to response

**Example - Simple Action:**

```typescript theme={null}
collection.addAction('Mark as verified', {
  scope: 'Single',
  execute: async (context, resultBuilder) => {
    const user = await context.getRecord(['id']);
    await updateUser(user.id, { verified: true });
    return resultBuilder.success('User marked as verified');
  },
});
```

**Example - Action with Form:**

```typescript theme={null}
collection.addAction('Send notification', {
  scope: 'Bulk',
  form: [
    {
      label: 'Message',
      type: 'String',
      isRequired: true,
    },
    {
      label: 'Channel',
      type: 'Enum',
      enumValues: ['email', 'sms', 'push'],
      isRequired: true,
    },
  ],
  execute: async (context, resultBuilder) => {
    const { message, channel } = context.formValues;
    const users = await context.getRecords(['email']);

    for (const user of users) {
      await sendNotification(user.email, message, channel);
    }

    return resultBuilder.success(`Sent ${channel} to ${users.length} users`);
  },
});
```

**Example - File Generation:**

```typescript theme={null}
collection.addAction('Export to PDF', {
  scope: 'Bulk',
  generateFile: true,
  execute: async (context, resultBuilder) => {
    const records = await context.getRecords(['name', 'email']);
    const pdfStream = await generatePDF(records);

    return resultBuilder.file(pdfStream, 'export.pdf', 'application/pdf');
  },
});
```

***

## Fields

### collection.addField(name, definition)

Add a computed field to the collection.

```typescript theme={null}
collection.addField(
  name: string,
  definition: ComputedDefinition
): CollectionCustomizer;
```

**Definition Properties:**

| Property       | Type       | Required | Description                    |
| -------------- | ---------- | -------- | ------------------------------ |
| `columnType`   | ColumnType | Yes      | Field data type                |
| `dependencies` | string\[]  | Yes      | Fields needed for computation  |
| `getValues`    | function   | Yes      | Value computation function     |
| `defaultValue` | any        | No       | Default value                  |
| `enumValues`   | string\[]  | 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
* `'Time'` - Time only
* `'Enum'` - Enumeration
* `'Json'` - JSON object
* `'Uuid'` - UUID
* `'Point'` - Geographic point
* `'File'` - File reference

**Example - Simple Computed Field:**

```typescript theme={null}
collection.addField('fullName', {
  columnType: 'String',
  dependencies: ['firstName', 'lastName'],
  getValues: (records) =>
    records.map(r => `${r.firstName} ${r.lastName}`),
});
```

**Example - Async Computed Field:**

```typescript theme={null}
collection.addField('revenueThisYear', {
  columnType: 'Number',
  dependencies: ['id'],
  getValues: async (records) => {
    const ids = records.map(r => r.id);
    const revenues = await fetchRevenues(ids);
    return revenues;
  },
});
```

***

### collection.importField(name, options)

Import a field from a related collection.

```typescript theme={null}
collection.importField(
  name: string,
  options: { path: string; readonly?: boolean }
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
// Import author's name into books collection
collection.importField('authorName', {
  path: 'author:fullName',
  readonly: true,
});
```

***

### collection.renameField(currentName, newName)

Rename a field in the exported schema.

```typescript theme={null}
collection.renameField(
  currentName: string,
  newName: string
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.renameField('created_at', 'createdAt');
```

***

### collection.removeField(...names)

Remove fields from the exported schema (they remain usable within the agent).

```typescript theme={null}
collection.removeField(...names: string[]): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.removeField('password', 'internalNotes', 'debugData');
```

***

### collection.addFieldValidation(name, operator, value?)

Add a validation rule to a field.

```typescript theme={null}
collection.addFieldValidation(
  name: string,
  operator: Operator,
  value?: any
): CollectionCustomizer;
```

**Operators:**

`'Present'`, `'LongerThan'`, `'ShorterThan'`, `'Contains'`, `'Like'`, `'Match'`, `'GreaterThan'`, `'LessThan'`, `'Before'`, `'After'`

**Example:**

```typescript theme={null}
collection
  .addFieldValidation('email', 'Present')
  .addFieldValidation('email', 'Match', /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/)
  .addFieldValidation('age', 'GreaterThan', 18)
  .addFieldValidation('username', 'LongerThan', 3);
```

***

### collection.setFieldNullable(name)

Mark a field as optional (nullable).

```typescript theme={null}
collection.setFieldNullable(name: string): CollectionCustomizer;
```

<Warning>
  Your database might still refuse empty values if it requires one.
</Warning>

**Example:**

```typescript theme={null}
collection.setFieldNullable('middleName');
```

***

### collection.replaceFieldWriting(name, definition)

Replace the write behavior of a field.

```typescript theme={null}
collection.replaceFieldWriting(
  name: string,
  definition: WriteDefinition
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
// Write fullName as firstName + lastName
collection.replaceFieldWriting('fullName', fullName => {
  const [firstName, lastName] = fullName.split(' ');
  return { firstName, lastName };
});
```

***

### collection.replaceFieldBinaryMode(name, mode)

Choose how binary data should be transported to the GUI.

```typescript theme={null}
collection.replaceFieldBinaryMode(
  name: string,
  mode: 'datauri' | 'hex'
): CollectionCustomizer;
```

**Modes:**

* `'datauri'` - Best for file uploads, uses FilePicker widget
* `'hex'` - Best for short binary data like UUIDs

**Example:**

```typescript theme={null}
collection.replaceFieldBinaryMode('avatar', 'datauri');
collection.replaceFieldBinaryMode('uuid', 'hex');
```

***

## Relationships

### collection.addManyToOneRelation(name, foreignCollection, options)

Add a many-to-one relationship.

```typescript theme={null}
collection.addManyToOneRelation(
  name: string,
  foreignCollection: string,
  options: {
    foreignKey: string;
    foreignKeyTarget?: string;
  }
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
// books.authorId → persons.id
books.addManyToOneRelation('author', 'persons', {
  foreignKey: 'authorId',
});
```

***

### collection.addOneToManyRelation(name, foreignCollection, options)

Add a one-to-many relationship.

```typescript theme={null}
collection.addOneToManyRelation(
  name: string,
  foreignCollection: string,
  options: {
    originKey: string;
    originKeyTarget?: string;
  }
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
// persons.id ← books.authorId
persons.addOneToManyRelation('writtenBooks', 'books', {
  originKey: 'authorId',
});
```

***

### collection.addOneToOneRelation(name, foreignCollection, options)

Add a one-to-one relationship.

```typescript theme={null}
collection.addOneToOneRelation(
  name: string,
  foreignCollection: string,
  options: {
    originKey: string;
    originKeyTarget?: string;
  }
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
// persons.id ← profiles.personId (unique)
persons.addOneToOneRelation('profile', 'profiles', {
  originKey: 'personId',
});
```

***

### collection.addManyToManyRelation(name, foreignCollection, throughCollection, options)

Add a many-to-many relationship.

```typescript theme={null}
collection.addManyToManyRelation(
  name: string,
  foreignCollection: string,
  throughCollection: string,
  options: {
    originKey: string;
    foreignKey: string;
    originKeyTarget?: string;
    foreignKeyTarget?: string;
  }
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
// students ↔ student_courses ↔ courses
students.addManyToManyRelation('enrolledCourses', 'courses', 'student_courses', {
  originKey: 'studentId',
  foreignKey: 'courseId',
});
```

***

### collection.addExternalRelation(name, definition)

Add a virtual collection into the related data of a record.

```typescript theme={null}
collection.addExternalRelation(
  name: string,
  definition: ExternalRelationDefinition
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.addExternalRelation('relatedProducts', {
  schema: { id: 'Number', name: 'String', price: 'Number' },
  listRecords: async ({ id }) => {
    return await fetchRelatedProducts(id);
  },
});
```

***

## Segments

### collection.addSegment(name, definition)

Add a segment (saved filter) to the collection.

```typescript theme={null}
collection.addSegment(
  name: string,
  definition: SegmentDefinition
): CollectionCustomizer;
```

**Example - Static Segment:**

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

**Example - Dynamic Segment:**

```typescript theme={null}
collection.addSegment('Active this month', async (context) => {
  const startOfMonth = new Date();
  startOfMonth.setDate(1);

  return {
    field: 'lastActiveAt',
    operator: 'After',
    value: startOfMonth,
  };
});
```

***

## Hooks

### collection.addHook(position, type, handler)

Add a hook to execute code before or after operations.

```typescript theme={null}
collection.addHook(
  position: 'Before' | 'After',
  type: HookType,
  handler: HookHandler
): CollectionCustomizer;
```

**Hook Types:**

`'List'`, `'Create'`, `'Update'`, `'Delete'`, `'Aggregate'`

**Example - Before Hook:**

```typescript theme={null}
collection.addHook('Before', 'Create', async (context) => {
  // Validate data before creation
  if (!context.data.email) {
    throw new Error('Email is required');
  }
});
```

**Example - After Hook:**

```typescript theme={null}
collection.addHook('After', 'Update', async (context) => {
  // Send notification after update
  const records = await context.collection.list(context.filter, ['email']);
  for (const record of records) {
    await sendUpdateNotification(record.email);
  }
});
```

***

## Search

### collection.replaceSearch(definition)

Replace the default search behavior.

```typescript theme={null}
collection.replaceSearch(
  definition: SearchDefinition
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.replaceSearch(async (searchString) => {
  // Search in multiple fields
  return {
    aggregator: 'Or',
    conditions: [
      { field: 'firstName', operator: 'Contains', value: searchString },
      { field: 'lastName', operator: 'Contains', value: searchString },
      { field: 'email', operator: 'Contains', value: searchString },
    ],
  };
});
```

***

### collection.disableSearch()

Disable search functionality on the collection.

```typescript theme={null}
collection.disableSearch(): CollectionCustomizer;
```

***

## Sorting

### collection.emulateFieldSorting(name)

Enable in-memory sorting on a field.

```typescript theme={null}
collection.emulateFieldSorting(name: string): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.emulateFieldSorting('fullName');
```

***

### collection.replaceFieldSorting(name, equivalentSort)

Replace sorting implementation for a field.

```typescript theme={null}
collection.replaceFieldSorting(
  name: string,
  equivalentSort: SortClause[]
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.replaceFieldSorting('fullName', [
  { field: 'lastName', ascending: true },
  { field: 'firstName', ascending: true },
]);
```

***

### collection.disableFieldSorting(name)

Disable sorting on a specific field.

```typescript theme={null}
collection.disableFieldSorting(name: string): CollectionCustomizer;
```

***

## Filtering

### collection.emulateFieldFiltering(name)

Enable in-memory filtering on all operators for a field.

```typescript theme={null}
collection.emulateFieldFiltering(name: string): CollectionCustomizer;
```

***

### collection.emulateFieldOperator(name, operator)

Enable in-memory filtering for a specific operator on a field.

```typescript theme={null}
collection.emulateFieldOperator(
  name: string,
  operator: Operator
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.emulateFieldOperator('fullName', 'Contains');
```

***

### collection.replaceFieldOperator(name, operator, replacer)

Replace the implementation of a filter operator.

```typescript theme={null}
collection.replaceFieldOperator(
  name: string,
  operator: Operator,
  replacer: OperatorDefinition
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.replaceFieldOperator('fullName', 'Contains', (value) => {
  return {
    aggregator: 'Or',
    conditions: [
      { field: 'firstName', operator: 'Contains', value },
      { field: 'lastName', operator: 'Contains', value },
    ],
  };
});
```

***

## Charts

### collection.addChart(name, definition)

Add a chart to the collection.

```typescript theme={null}
collection.addChart(
  name: string,
  definition: ChartDefinition
): CollectionCustomizer;
```

**Example - Value Chart:**

```typescript theme={null}
collection.addChart('totalRevenue', async (context, resultBuilder) => {
  const total = await calculateTotalRevenue();
  return resultBuilder.value(total);
});
```

**Example - Distribution Chart:**

```typescript theme={null}
collection.addChart('usersByPlan', async (context, resultBuilder) => {
  const distribution = await getUserDistributionByPlan();
  return resultBuilder.distribution(distribution);
});
```

**Example - Time-based Chart:**

```typescript theme={null}
collection.addChart('signupsOverTime', async (context, resultBuilder) => {
  const data = await getSignupsOverTime(context.timezone);
  return resultBuilder.timeBased(data);
});
```

***

## Collection Overrides

### collection.overrideCreate(handler)

Replace the default create operation.

```typescript theme={null}
collection.overrideCreate(
  handler: CreateOverrideHandler
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.overrideCreate(async (context) => {
  const { data } = context;

  // Custom creation logic
  const record = await customCreateAPI(data);

  return [record];
});
```

***

### collection.overrideUpdate(handler)

Replace the default update operation.

```typescript theme={null}
collection.overrideUpdate(
  handler: UpdateOverrideHandler
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.overrideUpdate(async (context) => {
  const { filter, patch } = context;

  // Custom update logic
  await customUpdateAPI(filter, patch);
});
```

***

### collection.overrideDelete(handler)

Replace the default delete operation.

```typescript theme={null}
collection.overrideDelete(
  handler: DeleteOverrideHandler
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
collection.overrideDelete(async (context) => {
  const { filter } = context;

  // Custom deletion logic (e.g., soft delete)
  await customSoftDeleteAPI(filter);
});
```

***

## Other Methods

### collection.disableCount()

Disable count in list view pagination for improved performance.

```typescript theme={null}
collection.disableCount(): CollectionCustomizer;
```

***

### collection.use(plugin, options?)

Load a plugin on a specific collection.

```typescript theme={null}
collection.use(
  plugin: Plugin,
  options?: any
): CollectionCustomizer;
```

**Example:**

```typescript theme={null}
import { createFileField } from '@forestadmin/plugin-s3';

collection.use(createFileField, {
  fieldname: 'avatar',
  bucket: 'my-bucket',
});
```

***

## Chart Result Builders

When creating charts with `collection.addChart()` or `agent.addChart()`, the result builder provides methods to format chart data.

### resultBuilder.value(value, previousValue?)

Create a Value chart (single number).

```typescript theme={null}
collection.addChart('totalRevenue', async (context, resultBuilder) => {
  const total = await calculateRevenue();
  const previous = await calculateRevenue(lastMonth);
  return resultBuilder.value(total, previous);
});
```

***

### resultBuilder.distribution(obj)

Create a Distribution/Pie chart.

```typescript theme={null}
collection.addChart('usersByPlan', async (context, resultBuilder) => {
  return resultBuilder.distribution({
    'Free': 1000,
    'Pro': 500,
    'Enterprise': 50,
  });
});
```

***

### resultBuilder.timeBased(timeRange, values)

Create a Time-based/Line chart.

```typescript theme={null}
collection.addChart('signupsOverTime', async (context, resultBuilder) => {
  return resultBuilder.timeBased('Day', [
    { date: new Date('2024-01-01'), value: 10 },
    { date: new Date('2024-01-02'), value: 15 },
    { date: new Date('2024-01-03'), value: null }, // Missing data
  ]);
});
```

**Time Ranges:** `'Day'`, `'Week'`, `'Month'`, `'Quarter'`, `'Year'`

***

### resultBuilder.multipleTimeBased(timeRange, dates, lines)

Create a Multi-line Time-based chart.

```typescript theme={null}
collection.addChart('comparison', async (context, resultBuilder) => {
  const dates = [new Date('2024-01-01'), new Date('2024-01-02'), new Date('2024-01-03')];

  return resultBuilder.multipleTimeBased('Day', dates, [
    { label: 'Sales', values: [100, 150, 200] },
    { label: 'Returns', values: [10, 15, null] },
  ]);
});
```

***

### resultBuilder.percentage(value)

Create a Percentage chart.

```typescript theme={null}
collection.addChart('completionRate', async (context, resultBuilder) => {
  const rate = (completed / total) * 100;
  return resultBuilder.percentage(rate);
});
```

***

### resultBuilder.objective(value, objective)

Create an Objective chart (progress toward goal).

```typescript theme={null}
collection.addChart('salesGoal', async (context, resultBuilder) => {
  const current = await getCurrentSales();
  const target = 100000;
  return resultBuilder.objective(current, target);
});
```

***

### resultBuilder.leaderboard(obj)

Create a Leaderboard chart (sorted distribution).

```typescript theme={null}
collection.addChart('topSellers', async (context, resultBuilder) => {
  return resultBuilder.leaderboard({
    'John': 5000,
    'Jane': 7500,
    'Bob': 3000,
  });
  // Automatically sorted: Jane (7500), John (5000), Bob (3000)
});
```

***

### resultBuilder.smart(data)

Create a Smart chart (custom format).

```typescript theme={null}
collection.addChart('custom', async (context, resultBuilder) => {
  return resultBuilder.smart({
    // Custom chart data structure
    type: 'custom',
    data: [/* your data */],
  });
});
```

***

## Form Field Types

Action forms support various field types with different widgets.

### Basic Field Types

```typescript theme={null}
collection.addAction('Example', {
  scope: 'Single',
  form: [
    {
      label: 'User Name',
      type: 'String',
      isRequired: true,
      description: 'Enter the user name',
    },
    {
      label: 'Age',
      type: 'Number',
      isRequired: false,
      defaultValue: 18,
    },
    {
      label: 'Is Active',
      type: 'Boolean',
      defaultValue: true,
    },
    {
      label: 'Birth Date',
      type: 'Date',
    },
    {
      label: 'Metadata',
      type: 'Json',
    },
  ],
  execute: async (context, resultBuilder) => {
    const { userName, age, isActive, birthDate, metadata } = context.formValues;
    // ... action logic
    return resultBuilder.success();
  },
});
```

**Available Types:**

* `'String'` - Text input
* `'Number'` - Numeric input
* `'Boolean'` - Checkbox
* `'Date'` - Date picker
* `'Dateonly'` - Date without time
* `'Time'` - Time picker
* `'Enum'` - Single selection
* `'EnumList'` - Multiple selection
* `'File'` - File upload
* `'FileList'` - Multiple file upload
* `'Json'` - JSON editor
* `'Collection'` - Record picker
* `'NumberList'` - Array of numbers
* `'StringList'` - Array of strings

***

### Enum Fields

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

***

### Collection Fields

Pick a record from another collection:

```typescript theme={null}
{
  label: 'Assign to User',
  type: 'Collection',
  collectionName: 'users',
}
```

***

### Dropdown Widget

```typescript theme={null}
{
  label: 'Country',
  type: 'String',
  widget: 'Dropdown',
  options: [
    { label: 'United States', value: 'US' },
    { label: 'United Kingdom', value: 'UK' },
    { label: 'France', value: 'FR' },
  ],
  placeholder: 'Select a country',
  search: 'static', // or 'disabled'
}
```

***

### Dynamic Dropdown

```typescript theme={null}
{
  label: 'Product',
  type: 'String',
  widget: 'Dropdown',
  search: 'dynamic',
  options: async (context, searchValue) => {
    const products = await searchProducts(searchValue);
    return products.map(p => ({
      label: p.name,
      value: p.id,
    }));
  },
}
```

***

### Conditional Fields

Show/hide fields based on other field values:

```typescript theme={null}
form: [
  {
    label: 'Notification Type',
    type: 'Enum',
    enumValues: ['email', 'sms', 'push'],
  },
  {
    label: 'Email Address',
    type: 'String',
    if: (context) => context.formValues.notificationType === 'email',
  },
  {
    label: 'Phone Number',
    type: 'String',
    if: (context) => context.formValues.notificationType === 'sms',
  },
]
```

***

### Dynamic Field Values

```typescript theme={null}
{
  label: 'Department',
  type: 'Enum',
  enumValues: async (context) => {
    // Fetch departments based on selected company
    const companyId = context.formValues.companyId;
    return await getDepartments(companyId);
  },
}
```

***

### Read-Only Fields

```typescript theme={null}
{
  label: 'Created At',
  type: 'Date',
  isReadOnly: true,
  value: async (context) => {
    const record = await context.getRecord(['createdAt']);
    return record.createdAt;
  },
}
```

***

## ConditionTree Utilities

Build complex filter conditions programmatically.

### ConditionTreeLeaf

Simple condition on a single field:

```typescript theme={null}
import { ConditionTreeLeaf } from '@forestadmin/agent';

const condition = new ConditionTreeLeaf('status', 'Equal', 'active');
// Equivalent to: { field: 'status', operator: 'Equal', value: 'active' }
```

***

### ConditionTreeBranch

Combine multiple conditions with AND/OR:

```typescript theme={null}
import { ConditionTreeBranch, ConditionTreeLeaf } from '@forestadmin/agent';

const condition = new ConditionTreeBranch('And', [
  new ConditionTreeLeaf('status', 'Equal', 'active'),
  new ConditionTreeLeaf('age', 'GreaterThan', 18),
]);
```

***

### ConditionTree Factory

```typescript theme={null}
import { ConditionTreeFactory } from '@forestadmin/agent';

// From plain object
const condition = ConditionTreeFactory.fromPlainObject({
  field: 'email',
  operator: 'Contains',
  value: '@example.com',
});

// Combine conditions
const combined = ConditionTreeFactory.intersect([
  condition1,
  condition2,
]);

// Union (OR)
const union = ConditionTreeFactory.union([
  condition1,
  condition2,
]);
```

***

### 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'`

**Array:**

* `'IncludesAll'`, `'IncludesNone'`

***

## Related Types

### Caller

User information available in all contexts:

```typescript theme={null}
interface Caller {
  id: number;
  email: string;
  firstName: string;
  lastName: string;
  team: string;
  role: string;
  tags: Record<string, string>;
  timezone: string;
}
```

***

### Filter

```typescript theme={null}
import { Filter, ConditionTreeLeaf } from '@forestadmin/agent';

const filter = new Filter({
  conditionTree: new ConditionTreeLeaf('status', 'Equal', 'active'),
  search: 'john',
  searchExtended: false,
  segment: 'premium-users',
});
```

***

### Projection

Specify which fields to retrieve:

```typescript theme={null}
import { Projection } from '@forestadmin/agent';

const projection = new Projection('id', 'name', 'email', 'company:name');
```

***

## Plugin: Flattener

`@forestadmin/plugin-flattener` flattens nested data structures (composite columns, relations, JSON columns) into individual top-level fields.

### flattenColumn(dataSource, collection, options)

Decompose a column with a composite type into individual fields.

```typescript theme={null}
import { flattenColumn } from '@forestadmin/plugin-flattener';

agent.customizeCollection('orders', async (collection) => {
  await collection.use(flattenColumn, {
    columnName: 'address',
    include: ['street', 'city', 'zipCode'],
    readonly: false,
  });
});
```

**Options:**

| Option       | Type      | Required | Description                           |
| ------------ | --------- | -------- | ------------------------------------- |
| `columnName` | string    | Yes      | Column to flatten                     |
| `include`    | string\[] | No       | Fields to import (defaults to all)    |
| `exclude`    | string\[] | No       | Fields to skip                        |
| `level`      | number    | No       | Maximum nesting depth                 |
| `readonly`   | boolean   | No       | Whether imported fields are read-only |
| `columnType` | object    | No       | Custom type mapping for nested fields |

***

### flattenRelation(dataSource, collection, options)

Import fields from a relation directly into the collection.

```typescript theme={null}
import { flattenRelation } from '@forestadmin/plugin-flattener';

agent.customizeCollection('books', async (collection) => {
  await collection.use(flattenRelation, {
    relationName: 'author',
    include: ['firstName', 'lastName', 'email'],
    readonly: true,
  });
});
```

**Options:**

| Option         | Type      | Required | Description                           |
| -------------- | --------- | -------- | ------------------------------------- |
| `relationName` | string    | Yes      | Relation to flatten                   |
| `include`      | string\[] | No       | Fields to import                      |
| `exclude`      | string\[] | No       | Fields to skip                        |
| `readonly`     | boolean   | No       | Whether imported fields are read-only |

***

### flattenJsonColumn(dataSource, collection, options)

Expand a JSON column into individual typed fields.

```typescript theme={null}
import { flattenJsonColumn } from '@forestadmin/plugin-flattener';

agent.customizeCollection('products', (collection) => {
  collection.use(flattenJsonColumn, {
    columnName: 'metadata',
    columnType: {
      weight: 'Number',
      dimensions: { width: 'Number', height: 'Number' },
      tags: ['String'],
    },
    readonly: false,
    keepOriginalColumn: false,
  });
});
```

**Options:**

| Option               | Type    | Required | Description                                  |
| -------------------- | ------- | -------- | -------------------------------------------- |
| `columnName`         | string  | Yes      | JSON column to flatten                       |
| `columnType`         | object  | Yes      | Type definition for nested fields            |
| `level`              | number  | No       | Maximum nesting depth                        |
| `readonly`           | boolean | No       | Whether flattened fields are read-only       |
| `keepOriginalColumn` | boolean | No       | Whether to preserve the original JSON column |

***

## Package: Forest Cloud

`@forestadmin/forest-cloud` is a dev-only package that provides CLI tooling for cloud-hosted customization projects.

### Installation

```bash theme={null}
npm install @forestadmin/forest-cloud --save-dev
```

### CLI Commands

#### bootstrap

Initialize a cloud customization project. Authenticates with Forest, creates a `cloud-customizer` directory, configures credentials, and generates type definitions.

```bash theme={null}
npx forest-cloud bootstrap --env-secret YOUR_FOREST_ENV_SECRET
```

#### update-typings

Regenerate TypeScript type definitions based on your current database structure and customization code.

```bash theme={null}
npx forest-cloud update-typings
```

#### login

Refresh your Forest authentication token.

```bash theme={null}
npx forest-cloud login
```

### Exported Types

```typescript theme={null}
import type { Agent, SqlConnectionParams, MongoConnectionParams } from '@forestadmin/forest-cloud';
```

| Type                    | Description                                              |
| ----------------------- | -------------------------------------------------------- |
| `Agent`                 | Re-export of the `Agent` class from `@forestadmin/agent` |
| `SqlConnectionParams`   | Connection options for SQL datasources                   |
| `MongoConnectionParams` | Connection parameters for MongoDB datasources            |

***

## Package: Agent Testing

`@forestadmin/agent-testing` provides utilities to test agent customizations locally without connecting to Forest servers.

### createForestServerSandbox(port)

Start a local sandbox that mimics Forest servers.

```typescript theme={null}
import { createForestServerSandbox } from '@forestadmin/agent-testing';

const sandbox = await createForestServerSandbox(3001);

// ... run your tests

await sandbox.close();
```

***

### createAgentTestClient(options)

Connect a test client to a running agent to simulate frontend requests.

```typescript theme={null}
import { createAgentTestClient } from '@forestadmin/agent-testing';

const client = await createAgentTestClient({
  agentForestEnvSecret: process.env.FOREST_ENV_SECRET,
  agentForestAuthSecret: process.env.FOREST_AUTH_SECRET,
  agentUrl: 'http://localhost:3000',
  serverUrl: 'http://localhost:3001',
  agentSchemaPath: '.forestadmin-schema.json',
});
```

**Parameters:**

| Option                  | Type   | Description                        |
| ----------------------- | ------ | ---------------------------------- |
| `agentForestEnvSecret`  | string | Environment secret                 |
| `agentForestAuthSecret` | string | Auth secret                        |
| `agentUrl`              | string | URL of your running agent          |
| `serverUrl`             | string | URL of the sandbox server          |
| `agentSchemaPath`       | string | Path to `.forestadmin-schema.json` |

### Typical test workflow

```typescript theme={null}
import { createForestServerSandbox, createAgentTestClient } from '@forestadmin/agent-testing';

// 1. Start sandbox
const sandbox = await createForestServerSandbox(3001);

// 2. Start your agent (pointing at sandbox)
// FOREST_SERVER_URL=http://localhost:3001 node index.js

// 3. Connect test client
const client = await createAgentTestClient({
  agentForestEnvSecret: 'test-env-secret',
  agentForestAuthSecret: 'test-auth-secret',
  agentUrl: 'http://localhost:3000',
  serverUrl: 'http://localhost:3001',
  agentSchemaPath: '.forestadmin-schema.json',
});

// 4. Assert
const users = await client.collection('users').list();
expect(users).toHaveLength(3);

// 5. Cleanup
await sandbox.close();
```
