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

# Context & scope

> Understanding action scopes and the context object

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

| Scope      | Targets                       | Triggered from list view              | Triggered from detail view |
| ---------- | ----------------------------- | ------------------------------------- | -------------------------- |
| **Single** | One record at a time          | When one record is selected           | ✅ Yes                      |
| **Bulk**   | Multiple selected records     | When one or more records are selected | ✅ Yes                      |
| **Global** | Your choice among all records | ✅ Always available                    | ❌ No                       |

### Single scope

Execute on one specific record.

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  collection.addAction('Send email', {
    scope: 'Single',
    execute: async (context, resultBuilder) => {
      const user = await context.getRecord(['email', 'name']);
      // Process single user
    },
  });
  ```

  ```ruby Ruby theme={null}
  collection.add_action(
    'Send email',
    BaseAction.new(scope: ActionScope::SINGLE) do |context, result_builder|
      user = context.get_record(['email', 'name'])
      # Process single user
    end
  )
  ```

  ```ruby Ruby DSL theme={null}
  collection.action 'Send email', scope: :single do
    execute do
      user = record(['email', 'name'])
      # Process single user
    end
  end
  ```
</CodeGroup>

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

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  collection.addAction('Archive selected', {
    scope: 'Bulk',
    execute: async (context, resultBuilder) => {
      const orders = await context.getRecords(['id']);
      // Process all selected orders
    },
  });
  ```

  ```ruby Ruby theme={null}
  collection.add_action(
    'Archive selected',
    BaseAction.new(scope: ActionScope::BULK) do |context, result_builder|
      orders = context.get_records(['id'])
      # Process all selected orders
    end
  )
  ```

  ```ruby Ruby DSL theme={null}
  collection.action 'Archive selected', scope: :bulk do
    execute do
      orders = records(['id'])
      # Process all selected orders
    end
  end
  ```
</CodeGroup>

**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

<Warning>
  Handle failures gracefully in bulk actions. Decide whether to stop on first error or continue processing remaining records.
</Warning>

### Global scope

Execute at collection level without selecting specific records.

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  collection.addAction('Import data', {
    scope: 'Global',
    execute: async (context, resultBuilder) => {
      const { file } = context.formValues;
      // Process import for entire collection
    },
  });
  ```

  ```ruby Ruby theme={null}
  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
  )
  ```

  ```ruby Ruby DSL theme={null}
  collection.action 'Import data', scope: :global do
    execute do
      file = form_value(:file)
      # Process import for entire collection
    end
  end
  ```
</CodeGroup>

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

```javascript theme={null}
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:

<CodeGroup>
  ```javascript Single action theme={null}
  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();
  ```

  ```javascript Bulk action theme={null}
  const users = await context.getRecords(['id', 'email']);
  // Returns array of records

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

### Current user

Access information about who triggered the action:

```javascript theme={null}
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:

```javascript theme={null}
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:

```javascript theme={null}
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):

```javascript theme={null}
if (context.hasFieldChanged('Status')) {
  // Status field was modified by user
  const newStatus = context.formValues.Status;
}
```

## Examples

### Example: Access record data

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  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>
        `,
      });
    },
  });
  ```

  ```ruby Ruby theme={null}
  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
  )
  ```
</CodeGroup>

### Example: Update selected records

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  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!');
    },
  });
  ```

  ```ruby Ruby theme={null}
  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
  )
  ```
</CodeGroup>

### Example: Bulk processing with error handling

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  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>
          `,
        });
      }
    },
  });
  ```

  ```ruby Ruby theme={null}
  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
  )
  ```
</CodeGroup>

### Example: Use user context

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  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}`);
    },
  });
  ```

  ```ruby Ruby theme={null}
  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
  )
  ```
</CodeGroup>

### Example: Global action with filters

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  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');
    },
  });
  ```

  ```ruby Ruby theme={null}
  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
  )
  ```
</CodeGroup>
