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
  },
});
collection.add_action(
  'Send email',
  BaseAction.new(scope: ActionScope::SINGLE) do |context, result_builder|
    user = context.get_record(['email', 'name'])
    # Process single user
  end
)
collection.action 'Send email', scope: :single do
  execute do
    user = record(['email', 'name'])
    # Process single user
  end
end
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
  },
});
collection.add_action(
  'Archive selected',
  BaseAction.new(scope: ActionScope::BULK) do |context, result_builder|
    orders = context.get_records(['id'])
    # Process all selected orders
  end
)
collection.action 'Archive selected', scope: :bulk do
  execute do
    orders = records(['id'])
    # Process all selected orders
  end
end
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
  },
});
collection.add_action(
  'Import data',
  BaseAction.new(scope: ActionScope::GLOBAL) do |context, result_builder|
    file = context.form_values['file']
    # Process import for entire collection
  end
)
collection.action 'Import data', scope: :global do
  execute do
    file = form_value(:file)
    # Process import for entire collection
  end
end
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();
const users = await context.getRecords(['id', 'email']);
// Returns array of records

const ids = await context.getRecordIds();
// Returns array of IDs: [1, 2, 3, ...]

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>
      `,
    });
  },
});
collection.add_action(
  'Display customer info',
  BaseAction.new(scope: ActionScope::SINGLE) do |context, result_builder|
    # Get specific fields
    customer = context.get_record([
      'firstName',
      'lastName',
      'email',
      'company:name'  # Relation field
    ])

    result_builder.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>"
    )
  end
)

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!');
  },
});
collection.add_action(
  'Mark as live',
  BaseAction.new(scope: ActionScope::SINGLE) do |context, result_builder|
    # Update using the filter
    context.collection.update(context.filter, {
      'status' => 'live',
      'liveAt' => Time.now
    })

    result_builder.success('Company is now live!')
  end
)

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>
        `,
      });
    }
  },
});
collection.add_action(
  'Send notifications',
  BaseAction.new(scope: ActionScope::BULK) do |context, result_builder|
    users = context.get_records(['id', 'email', 'name'])

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

    users.each do |user|
      begin
        send_notification(user['email'], user['name'])
        results[:success] << user['name']
      rescue => error
        results[:failed] << user['name']
      end
    end

    if results[:failed].empty?
      result_builder.success("Sent #{results[:success].length} notifications")
    else
      result_builder.error(
        'Some notifications failed',
        html: "<p>Success: #{results[:success].length}</p>
          <p>Failed: #{results[:failed].length}</p>
          <p>Failed users: #{results[:failed].join(', ')}</p>"
      )
    end
  end
)

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}`);
  },
});
collection.add_action(
  'Assign to me',
  BaseAction.new(scope: ActionScope::SINGLE) do |context, result_builder|
    ticket_id = context.get_record_id

    # Assign ticket to current user
    context.collection.update(context.filter, {
      'assignedTo' => context.caller.id,
      'assignedAt' => Time.now
    })

    result_builder.success("Ticket assigned to #{context.caller.email}")
  end
)

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');
  },
});
collection.add_action(
  'Export filtered data',
  BaseAction.new(
    scope: ActionScope::GLOBAL,
    is_generate_file: true
  ) do |context, result_builder|
    # Get records matching current filters
    records = context.collection.list(
      context.filter,
      ['id', 'name', 'email', 'status']
    )

    # Generate CSV
    csv = generate_csv(records)

    result_builder.file(content: csv, name: 'export.csv', mime_type: 'text/csv')
  end
)