Skip to main content
Actions have three scopes that determine how they trigger and which records they target. The context object provides access to form values, selected records, and user information.

Action scopes

ScopeTargetsTriggered from list viewTriggered from detail view
SingleOne record at a timeWhen one record is selected✅ Yes
BulkMultiple selected recordsWhen one or more records are selected✅ Yes
GlobalYour choice among all records✅ Always available❌ No

Single scope

Execute on one specific record.
collection.addAction('Send email', {
  scope: 'Single',
  execute: async (context, resultBuilder) => {
    const user = await context.getRecord(['email', 'name']);
    // Process single user
  },
});
Use when:
  • Sending an email to a specific user
  • Generating an invoice for one order
  • Viewing details of a single item
  • Resetting a user’s password
Context methods:
  • context.getRecord(fieldNames) - Get the selected record
  • context.getRecordId() - Get the record ID

Bulk scope

Execute on multiple selected records simultaneously.
collection.addAction('Archive selected', {
  scope: 'Bulk',
  execute: async (context, resultBuilder) => {
    const orders = await context.getRecords(['id']);
    // Process all selected orders
  },
});
Use when:
  • Archiving multiple items
  • Updating status of several records
  • Sending mass emails
  • Bulk deleting records
Context methods:
  • context.getRecords(fieldNames) - Get all selected records
  • context.getRecordIds() - Get array of record IDs
Handle failures gracefully in bulk actions. Decide whether to stop on first error or continue processing remaining records.

Global scope

Execute at collection level without selecting specific records.
collection.addAction('Import data', {
  scope: 'Global',
  execute: async (context, resultBuilder) => {
    const { file } = context.formValues;
    // Process import for entire collection
  },
});
Use when:
  • Importing data from CSV
  • Generating collection-wide reports
  • Syncing with external services
  • Running maintenance tasks
Context methods:
  • context.filter - Access current filters and search
  • context.collection - Query the collection
  • No specific records are pre-selected

The context object

The context object is passed as the first argument to the execute handler and provides access to all action data.

Form values

Access values entered by the user in the form:
const { Amount, Description } = context.formValues;

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

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

Selected records

Get data from the records the action is running on:
const user = await context.getRecord(['id', 'email', 'name']);
console.log(user.id, user.email, user.name);

// Get just the ID
const userId = await context.getRecordId();

Current user

Access information about who triggered the action:
const userId = context.caller.id;
const userEmail = context.caller.email;
const userRole = context.caller.role;
const userTeam = context.caller.team;
const userTimezone = context.caller.timezone;

Collection metadata

Access the collection schema and query interface:
const collectionName = context.collection.name;
const fields = context.collection.schema.fields;

// Query the collection
const records = await context.collection.list(filter, projection);

Filters

For Bulk and Global actions, access current filters from the UI:
const filter = context.filter;

// Use filter to query matching records
const matchingRecords = await context.collection.list(filter, projection);
This filter represents the current segment, search, and filters applied in the Forest interface.

Change detection

Check if a form field value has changed (useful for dynamic forms):
if (context.hasFieldChanged('Status')) {
  // Status field was modified by user
  const newStatus = context.formValues.Status;
}

Examples

Example: Access record data

collection.addAction('Display customer info', {
  scope: 'Single',
  execute: async (context, resultBuilder) => {
    // Get specific fields
    const customer = await context.getRecord([
      'firstName',
      'lastName',
      'email',
      'company:name'  // Relation field
    ]);

    return resultBuilder.success('Customer info', {
      html: `
        <p><strong>Name:</strong> ${customer.firstName} ${customer.lastName}</p>
        <p><strong>Email:</strong> ${customer.email}</p>
        <p><strong>Company:</strong> ${customer.company.name}</p>
      `,
    });
  },
});

Example: Update selected records

collection.addAction('Mark as live', {
  scope: 'Single',
  execute: async (context, resultBuilder) => {
    // Update using the filter
    await context.collection.update(context.filter, {
      status: 'live',
      liveAt: new Date(),
    });

    return resultBuilder.success('Company is now live!');
  },
});

Example: Bulk processing with error handling

collection.addAction('Send notifications', {
  scope: 'Bulk',
  execute: async (context, resultBuilder) => {
    const users = await context.getRecords(['id', 'email', 'name']);

    const results = { success: [], failed: [] };

    for (const user of users) {
      try {
        await sendNotification(user.email, user.name);
        results.success.push(user.name);
      } catch (error) {
        results.failed.push(user.name);
      }
    }

    if (results.failed.length === 0) {
      return resultBuilder.success(
        `Sent ${results.success.length} notifications`
      );
    } else {
      return resultBuilder.error('Some notifications failed', {
        html: `
          <p>Success: ${results.success.length}</p>
          <p>Failed: ${results.failed.length}</p>
          <p>Failed users: ${results.failed.join(', ')}</p>
        `,
      });
    }
  },
});

Example: Use user context

collection.addAction('Assign to me', {
  scope: 'Single',
  execute: async (context, resultBuilder) => {
    const ticketId = await context.getRecordId();

    // Assign ticket to current user
    await context.collection.update(context.filter, {
      assignedTo: context.caller.id,
      assignedAt: new Date(),
    });

    return resultBuilder.success(`Ticket assigned to ${context.caller.email}`);
  },
});

Example: Global action with filters

collection.addAction('Export filtered data', {
  scope: 'Global',
  generateFile: true,
  execute: async (context, resultBuilder) => {
    // Get records matching current filters
    const records = await context.collection.list(
      context.filter,
      ['id', 'name', 'email', 'status']
    );

    // Generate CSV
    const csv = generateCSV(records);

    return resultBuilder.file(csv, 'export.csv', 'text/csv');
  },
});