Skip to main content
Complete API reference for @forestadmin/agent (Node.js/TypeScript).

Agent Class

createAgent(options)

Create and configure a Forest agent.
import { createAgent } from '@forestadmin/agent';

const agent = createAgent(options: AgentOptions): Agent;
Parameters:
OptionTypeRequiredDescription
authSecretstringYesYour FOREST_AUTH_SECRET
envSecretstringYesYour FOREST_ENV_SECRET
isProductionbooleanNoEnable production mode
loggerfunctionNoCustom logger function
loggerLevelstringNoLog level: ‘Debug’, ‘Info’, ‘Warn’, ‘Error’
prefixstringNoAPI prefix (default: ‘/forest’)
schemaPathstringNoPath to .forestadmin-schema.json
Example:
const agent = createAgent({
  authSecret: process.env.FOREST_AUTH_SECRET,
  envSecret: process.env.FOREST_ENV_SECRET,
  isProduction: process.env.NODE_ENV === 'production',
});

agent.addDataSource(factory, options?)

Add a datasource to the agent.
agent.addDataSource(
  factory: DataSourceFactory,
  options?: DataSourceOptions
): Agent;
Parameters:
OptionTypeDescription
factoryDataSourceFactoryDatasource factory function
options.includestring[]Collections to include
options.excludestring[]Collections to exclude
options.renameRecord<string, string>Rename collections
Example:
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.
agent.customizeCollection(
  name: string,
  callback: (collection: CollectionCustomizer) => void
): Agent;
Example:
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.
agent.addChart(
  name: string,
  definition: DataSourceChartDefinition
): Agent;
Example:
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).
agent.removeCollection(...names: string[]): Agent;
Example:
agent.removeCollection('internalLogs', 'debugData');

agent.use(plugin, options?)

Load a plugin across all collections.
agent.use<Options>(
  plugin: Plugin<Options>,
  options?: Options
): Agent;
Example:
import advancedExportPlugin from '@forestadmin/plugin-export-advanced';

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

agent.start()

Start the agent and connect to Forest servers.
await agent.start(): Promise<void>;
Example:
await agent.start();
console.log('Agent started successfully');

agent.stop()

Stop the agent and close all connections.
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).
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 for the full guide.
agent.addAi(provider: AiProviderDefinition): Agent;
Can only be called once, calling it a second time throws an error. Example:
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.
agent.mountAiMcpServer(options?: {
  enabledTools?: ToolName[];
}): Agent;
Parameters:
OptionTypeDescription
enabledToolsToolName[]Restrict which MCP tools are exposed. Defaults to all tools.
Available tool names: 'describeCollection', 'list', 'listRelated', 'create', 'update', 'delete', 'associate', 'dissociate', 'getActionForm', 'executeAction' Example:
agent.mountAiMcpServer({
  enabledTools: ['describeCollection', 'list', 'listRelated'],
});
The MCP server exposes HTTP endpoints for OAuth and protocol communication:
EndpointPurpose
POST /mcpMain MCP protocol endpoint (Bearer token required)
POST /oauth/authorizeOAuth authorization
POST /oauth/tokenToken exchange
GET /.well-known/oauth-protected-resource/mcpOAuth 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.
await agent.updateTypesOnFileSystem(
  typingsPath: string,
  typingsMaxDepth: number
): Promise<void>;
Example:
await agent.updateTypesOnFileSystem('./typings.ts', 5);

Collection Customizer

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

Actions

collection.addAction(name, definition)

Add an action to the collection.
collection.addAction(
  name: string,
  definition: ActionDefinition
): CollectionCustomizer;
Definition Properties:
PropertyTypeDescription
scope’Single’ | ‘Bulk’ | ‘Global’Action scope
executefunctionAction execution handler
formFormElement[]Dynamic form configuration
descriptionstringAction description
generateFilebooleanWhether action returns a file
submitButtonLabelstringCustom button text
Execute Function:
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:
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:
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:
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.
collection.addField(
  name: string,
  definition: ComputedDefinition
): CollectionCustomizer;
Definition Properties:
PropertyTypeRequiredDescription
columnTypeColumnTypeYesField data type
dependenciesstring[]YesFields needed for computation
getValuesfunctionYesValue computation function
defaultValueanyNoDefault value
enumValuesstring[]NoEnum 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:
collection.addField('fullName', {
  columnType: 'String',
  dependencies: ['firstName', 'lastName'],
  getValues: (records) =>
    records.map(r => `${r.firstName} ${r.lastName}`),
});
Example - Async Computed Field:
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.
collection.importField(
  name: string,
  options: { path: string; readonly?: boolean }
): CollectionCustomizer;
Example:
// 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.
collection.renameField(
  currentName: string,
  newName: string
): CollectionCustomizer;
Example:
collection.renameField('created_at', 'createdAt');

collection.removeField(…names)

Remove fields from the exported schema (they remain usable within the agent).
collection.removeField(...names: string[]): CollectionCustomizer;
Example:
collection.removeField('password', 'internalNotes', 'debugData');

collection.addFieldValidation(name, operator, value?)

Add a validation rule to a field.
collection.addFieldValidation(
  name: string,
  operator: Operator,
  value?: any
): CollectionCustomizer;
Operators: 'Present', 'LongerThan', 'ShorterThan', 'Contains', 'Like', 'Match', 'GreaterThan', 'LessThan', 'Before', 'After' Example:
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).
collection.setFieldNullable(name: string): CollectionCustomizer;
Your database might still refuse empty values if it requires one.
Example:
collection.setFieldNullable('middleName');

collection.replaceFieldWriting(name, definition)

Replace the write behavior of a field.
collection.replaceFieldWriting(
  name: string,
  definition: WriteDefinition
): CollectionCustomizer;
Example:
// 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.
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:
collection.replaceFieldBinaryMode('avatar', 'datauri');
collection.replaceFieldBinaryMode('uuid', 'hex');

Relationships

collection.addManyToOneRelation(name, foreignCollection, options)

Add a many-to-one relationship.
collection.addManyToOneRelation(
  name: string,
  foreignCollection: string,
  options: {
    foreignKey: string;
    foreignKeyTarget?: string;
  }
): CollectionCustomizer;
Example:
// books.authorId → persons.id
books.addManyToOneRelation('author', 'persons', {
  foreignKey: 'authorId',
});

collection.addOneToManyRelation(name, foreignCollection, options)

Add a one-to-many relationship.
collection.addOneToManyRelation(
  name: string,
  foreignCollection: string,
  options: {
    originKey: string;
    originKeyTarget?: string;
  }
): CollectionCustomizer;
Example:
// persons.id ← books.authorId
persons.addOneToManyRelation('writtenBooks', 'books', {
  originKey: 'authorId',
});

collection.addOneToOneRelation(name, foreignCollection, options)

Add a one-to-one relationship.
collection.addOneToOneRelation(
  name: string,
  foreignCollection: string,
  options: {
    originKey: string;
    originKeyTarget?: string;
  }
): CollectionCustomizer;
Example:
// persons.id ← profiles.personId (unique)
persons.addOneToOneRelation('profile', 'profiles', {
  originKey: 'personId',
});

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

Add a many-to-many relationship.
collection.addManyToManyRelation(
  name: string,
  foreignCollection: string,
  throughCollection: string,
  options: {
    originKey: string;
    foreignKey: string;
    originKeyTarget?: string;
    foreignKeyTarget?: string;
  }
): CollectionCustomizer;
Example:
// 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.
collection.addExternalRelation(
  name: string,
  definition: ExternalRelationDefinition
): CollectionCustomizer;
Example:
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.
collection.addSegment(
  name: string,
  definition: SegmentDefinition
): CollectionCustomizer;
Example - Static Segment:
collection.addSegment('Premium users', {
  field: 'plan',
  operator: 'Equal',
  value: 'premium',
});
Example - Dynamic Segment:
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.
collection.addHook(
  position: 'Before' | 'After',
  type: HookType,
  handler: HookHandler
): CollectionCustomizer;
Hook Types: 'List', 'Create', 'Update', 'Delete', 'Aggregate' Example - Before Hook:
collection.addHook('Before', 'Create', async (context) => {
  // Validate data before creation
  if (!context.data.email) {
    throw new Error('Email is required');
  }
});
Example - After Hook:
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);
  }
});

collection.replaceSearch(definition)

Replace the default search behavior.
collection.replaceSearch(
  definition: SearchDefinition
): CollectionCustomizer;
Example:
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.
collection.disableSearch(): CollectionCustomizer;

Sorting

collection.emulateFieldSorting(name)

Enable in-memory sorting on a field.
collection.emulateFieldSorting(name: string): CollectionCustomizer;
Example:
collection.emulateFieldSorting('fullName');

collection.replaceFieldSorting(name, equivalentSort)

Replace sorting implementation for a field.
collection.replaceFieldSorting(
  name: string,
  equivalentSort: SortClause[]
): CollectionCustomizer;
Example:
collection.replaceFieldSorting('fullName', [
  { field: 'lastName', ascending: true },
  { field: 'firstName', ascending: true },
]);

collection.disableFieldSorting(name)

Disable sorting on a specific field.
collection.disableFieldSorting(name: string): CollectionCustomizer;

Filtering

collection.emulateFieldFiltering(name)

Enable in-memory filtering on all operators for a field.
collection.emulateFieldFiltering(name: string): CollectionCustomizer;

collection.emulateFieldOperator(name, operator)

Enable in-memory filtering for a specific operator on a field.
collection.emulateFieldOperator(
  name: string,
  operator: Operator
): CollectionCustomizer;
Example:
collection.emulateFieldOperator('fullName', 'Contains');

collection.replaceFieldOperator(name, operator, replacer)

Replace the implementation of a filter operator.
collection.replaceFieldOperator(
  name: string,
  operator: Operator,
  replacer: OperatorDefinition
): CollectionCustomizer;
Example:
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.
collection.addChart(
  name: string,
  definition: ChartDefinition
): CollectionCustomizer;
Example - Value Chart:
collection.addChart('totalRevenue', async (context, resultBuilder) => {
  const total = await calculateTotalRevenue();
  return resultBuilder.value(total);
});
Example - Distribution Chart:
collection.addChart('usersByPlan', async (context, resultBuilder) => {
  const distribution = await getUserDistributionByPlan();
  return resultBuilder.distribution(distribution);
});
Example - Time-based Chart:
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.
collection.overrideCreate(
  handler: CreateOverrideHandler
): CollectionCustomizer;
Example:
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.
collection.overrideUpdate(
  handler: UpdateOverrideHandler
): CollectionCustomizer;
Example:
collection.overrideUpdate(async (context) => {
  const { filter, patch } = context;

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

collection.overrideDelete(handler)

Replace the default delete operation.
collection.overrideDelete(
  handler: DeleteOverrideHandler
): CollectionCustomizer;
Example:
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.
collection.disableCount(): CollectionCustomizer;

collection.use(plugin, options?)

Load a plugin on a specific collection.
collection.use(
  plugin: Plugin,
  options?: any
): CollectionCustomizer;
Example:
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).
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.
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.
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.
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.
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).
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).
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).
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

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

{
  label: 'Status',
  type: 'Enum',
  enumValues: ['pending', 'approved', 'rejected'],
  isRequired: true,
}

Collection Fields

Pick a record from another collection:
{
  label: 'Assign to User',
  type: 'Collection',
  collectionName: 'users',
}

{
  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

{
  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:
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

{
  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

{
  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:
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:
import { ConditionTreeBranch, ConditionTreeLeaf } from '@forestadmin/agent';

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

ConditionTree Factory

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'

Caller

User information available in all contexts:
interface Caller {
  id: number;
  email: string;
  firstName: string;
  lastName: string;
  team: string;
  role: string;
  tags: Record<string, string>;
  timezone: string;
}

Filter

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:
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.
import { flattenColumn } from '@forestadmin/plugin-flattener';

agent.customizeCollection('orders', async (collection) => {
  await collection.use(flattenColumn, {
    columnName: 'address',
    include: ['street', 'city', 'zipCode'],
    readonly: false,
  });
});
Options:
OptionTypeRequiredDescription
columnNamestringYesColumn to flatten
includestring[]NoFields to import (defaults to all)
excludestring[]NoFields to skip
levelnumberNoMaximum nesting depth
readonlybooleanNoWhether imported fields are read-only
columnTypeobjectNoCustom type mapping for nested fields

flattenRelation(dataSource, collection, options)

Import fields from a relation directly into the collection.
import { flattenRelation } from '@forestadmin/plugin-flattener';

agent.customizeCollection('books', async (collection) => {
  await collection.use(flattenRelation, {
    relationName: 'author',
    include: ['firstName', 'lastName', 'email'],
    readonly: true,
  });
});
Options:
OptionTypeRequiredDescription
relationNamestringYesRelation to flatten
includestring[]NoFields to import
excludestring[]NoFields to skip
readonlybooleanNoWhether imported fields are read-only

flattenJsonColumn(dataSource, collection, options)

Expand a JSON column into individual typed fields.
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:
OptionTypeRequiredDescription
columnNamestringYesJSON column to flatten
columnTypeobjectYesType definition for nested fields
levelnumberNoMaximum nesting depth
readonlybooleanNoWhether flattened fields are read-only
keepOriginalColumnbooleanNoWhether 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

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.
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.
npx forest-cloud update-typings

login

Refresh your Forest authentication token.
npx forest-cloud login

Exported Types

import type { Agent, SqlConnectionParams, MongoConnectionParams } from '@forestadmin/forest-cloud';
TypeDescription
AgentRe-export of the Agent class from @forestadmin/agent
SqlConnectionParamsConnection options for SQL datasources
MongoConnectionParamsConnection 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.
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.
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:
OptionTypeDescription
agentForestEnvSecretstringEnvironment secret
agentForestAuthSecretstringAuth secret
agentUrlstringURL of your running agent
serverUrlstringURL of the sandbox server
agentSchemaPathstringPath to .forestadmin-schema.json

Typical test workflow

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();