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

# Overview

> Build advanced actions with custom code for complex business logic

Code-based actions let you implement custom business logic in your back-end code. They provide full control over action behavior, forms, validation, and results.

Use code-based actions when you need complex workflows, dynamic forms, external API integrations, file generation, or custom validation that goes beyond what no-code actions can do.

<Frame caption="A custom action displayed in a Forest table view">
  <img src="https://mintcdn.com/forest-chore-open-api/DwOJ-XBdKEod-4Pc/images/actions/actions-dropdown.png?fit=max&auto=format&n=DwOJ-XBdKEod-4Pc&q=85&s=3f649f9255225a42e8ec1b97893c0ce0" alt="Custom action displayed in a table view" width="1920" height="391" data-path="images/actions/actions-dropdown.png" />
</Frame>

## Basic structure

Here's what a code-based action looks like:

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  agent.customizeCollection('companies', collection =>
    collection.addAction('Mark as Live', {
      scope: 'Single',
      description: 'Mark the company as live',
      form: [
        {
          type: 'Date',
          label: 'Live date',
          isRequired: true,
        },
        {
          type: 'String',
          label: 'Notes',
          widget: 'TextArea',
        },
      ],
      execute: async (context, resultBuilder) => {
        // Access form values
        const { 'Live date': liveDate, Notes: notes } = context.formValues;

        // Access selected record
        const company = await context.getRecord(['id', 'name']);

        // Perform business logic
        await markCompanyAsLive(company.id, liveDate, notes);

        // Return result
        return resultBuilder.success(`${company.name} is now live!`);
      },
    }),
  );
  ```

  ```ruby Ruby theme={null}
  include ForestAdminDatasourceCustomizer::Decorators::Action
  include ForestAdminDatasourceCustomizer::Decorators::Action::Types

  forest_agent.customize_collection('companies') do |collection|
    collection.add_action(
      'Mark as Live',
      BaseAction.new(
        scope: ActionScope::SINGLE,
        description: 'Mark the company as live',
        form: [
          {
            type: FieldType::DATE,
            label: 'Live date',
            is_required: true
          },
          {
            type: FieldType::STRING,
            label: 'Notes',
            widget: 'TextArea'
          }
        ]
      ) do |context, result_builder|
        # Access form values
        live_date = context.form_values['Live date']
        notes = context.form_values['Notes']

        # Access selected record
        company = context.get_record(['id', 'name'])

        # Perform business logic
        mark_company_as_live(company['id'], live_date, notes)

        # Return result
        result_builder.success("#{company['name']} is now live!")
      end
    )
  end
  ```

  ```ruby Ruby DSL theme={null}
  @create_agent.collection :companies do |collection|
    collection.action 'Mark as Live', scope: :single do
      description 'Mark the company as live'

      form do
        field :live_date, type: :date, is_required: true
        field :notes, type: :string, widget: 'TextArea'
      end

      execute do
        # Access form values
        live_date = form_value(:live_date)
        notes = form_value(:notes)

        # Access selected record
        company = record(['id', 'name'])

        # Perform business logic
        mark_company_as_live(company['id'], live_date, notes)

        # Return result
        success "#{company['name']} is now live!"
      end
    end
  end
  ```
</CodeGroup>

## Common examples

### Send email

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  collection.addAction('Send Welcome Email', {
    scope: 'Single',
    form: [
      { type: 'String', label: 'Subject', isRequired: true },
      { type: 'String', label: 'Message', widget: 'TextArea', isRequired: true },
    ],
    execute: async (context, resultBuilder) => {
      const user = await context.getRecord(['email', 'name']);
      const { Subject, Message } = context.formValues;

      try {
        await sendEmail({
          to: user.email,
          subject: Subject,
          body: Message,
        });
        return resultBuilder.success(`Email sent to ${user.email}`);
      } catch (error) {
        return resultBuilder.error(`Failed to send email: ${error.message}`);
      }
    },
  });
  ```

  ```ruby Ruby theme={null}
  collection.add_action(
    'Send Welcome Email',
    BaseAction.new(
      scope: ActionScope::SINGLE,
      form: [
        { type: FieldType::STRING, label: 'Subject', is_required: true },
        { type: FieldType::STRING, label: 'Message', widget: 'TextArea', is_required: true }
      ]
    ) do |context, result_builder|
      user = context.get_record(['email', 'name'])
      subject = context.form_values['Subject']
      message = context.form_values['Message']

      begin
        send_email(
          to: user['email'],
          subject: subject,
          body: message
        )
        result_builder.success("Email sent to #{user['email']}")
      rescue => error
        result_builder.error("Failed to send email: #{error.message}")
      end
    end
  )
  ```
</CodeGroup>

### Charge credit card

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  collection.addAction('Charge Credit Card', {
    scope: 'Single',
    form: [
      { type: 'Number', label: 'Amount', isRequired: true },
      { type: 'String', label: 'Description', isRequired: true },
    ],
    execute: async (context, resultBuilder) => {
      const customer = await context.getRecord(['stripeId', 'email']);
      const { Amount, Description } = context.formValues;

      try {
        const charge = await stripe.charges.create({
          amount: Amount * 100,
          currency: 'usd',
          customer: customer.stripeId,
          description: Description,
        });

        return resultBuilder.success('Charge successful', {
          html: `
            <p>Charged $${Amount} to ${customer.email}</p>
            <p>Transaction ID: ${charge.id}</p>
          `,
        });
      } catch (error) {
        return resultBuilder.error(`Charge failed: ${error.message}`);
      }
    },
  });
  ```

  ```ruby Ruby theme={null}
  collection.add_action(
    'Charge Credit Card',
    BaseAction.new(
      scope: ActionScope::SINGLE,
      form: [
        { type: FieldType::NUMBER, label: 'Amount', is_required: true },
        { type: FieldType::STRING, label: 'Description', is_required: true }
      ]
    ) do |context, result_builder|
      customer = context.get_record(['stripeId', 'email'])
      amount = context.form_values['Amount']
      description = context.form_values['Description']

      begin
        charge = Stripe::Charge.create(
          amount: (amount * 100).to_i,
          currency: 'usd',
          customer: customer['stripeId'],
          description: description
        )

        result_builder.success(
          'Charge successful',
          html: "<p>Charged $#{amount} to #{customer['email']}</p>
                 <p>Transaction ID: #{charge.id}</p>"
        )
      rescue => error
        result_builder.error("Charge failed: #{error.message}")
      end
    end
  )
  ```
</CodeGroup>

### Bulk status update

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  collection.addAction('Update Status', {
    scope: 'Bulk',
    form: [
      {
        type: 'Enum',
        label: 'New Status',
        enumValues: ['pending', 'approved', 'rejected'],
        isRequired: true,
      },
    ],
    execute: async (context, resultBuilder) => {
      const orders = await context.getRecords(['id']);
      const newStatus = context.formValues['New Status'];

      const ids = orders.map(o => o.id);
      await Order.update({ status: newStatus }, { where: { id: ids } });

      return resultBuilder.success(`Updated ${ids.length} orders to ${newStatus}`);
    },
  });
  ```

  ```ruby Ruby theme={null}
  collection.add_action(
    'Update Status',
    BaseAction.new(
      scope: ActionScope::BULK,
      form: [
        {
          type: FieldType::ENUM,
          label: 'New Status',
          enum_values: ['pending', 'approved', 'rejected'],
          is_required: true
        }
      ]
    ) do |context, result_builder|
      orders = context.get_records(['id'])
      new_status = context.form_values['New Status']

      ids = orders.map { |o| o['id'] }
      Order.where(id: ids).update_all(status: new_status)

      result_builder.success("Updated #{ids.length} orders to #{new_status}")
    end
  )
  ```

  ```ruby Ruby DSL theme={null}
  collection.action 'Update Status', scope: :bulk do
    form do
      field :new_status, type: :enum, enum_values: ['pending', 'approved', 'rejected'], is_required: true
    end

    execute do
      orders = records(['id'])
      new_status = form_value(:new_status)

      ids = orders.map { |o| o['id'] }
      Order.where(id: ids).update_all(status: new_status)

      success "Updated #{ids.length} orders to #{new_status}"
    end
  end
  ```
</CodeGroup>

### Generate report

<CodeGroup>
  ```javascript Node.js / Cloud theme={null}
  collection.addAction('Generate Report', {
    scope: 'Global',
    generateFile: true,
    form: [
      { type: 'DateOnly', label: 'Start Date', isRequired: true },
      { type: 'DateOnly', label: 'End Date', isRequired: true },
    ],
    execute: async (context, resultBuilder) => {
      const { 'Start Date': startDate, 'End Date': endDate } = context.formValues;

      const data = await fetchReportData(startDate, endDate);
      const pdf = await generatePDF(data);

      return resultBuilder.file(
        pdf,
        `report-${startDate}-${endDate}.pdf`,
        'application/pdf'
      );
    },
  });
  ```

  ```ruby Ruby theme={null}
  collection.add_action(
    'Generate Report',
    BaseAction.new(
      scope: ActionScope::GLOBAL,
      is_generate_file: true,
      form: [
        { type: FieldType::DATE_ONLY, label: 'Start Date', is_required: true },
        { type: FieldType::DATE_ONLY, label: 'End Date', is_required: true }
      ]
    ) do |context, result_builder|
      start_date = context.form_values['Start Date']
      end_date = context.form_values['End Date']

      data = fetch_report_data(start_date, end_date)
      pdf = generate_pdf(data)

      result_builder.file(
        content: pdf,
        name: "report-#{start_date}-#{end_date}.pdf",
        mime_type: 'application/pdf'
      )
    end
  )
  ```
</CodeGroup>

## Learn more

<CardGroup cols={2}>
  <Card title="Forms" icon="rectangle-list" href="/product/process/actions/custom-actions/forms">
    Form fields, validation, and dynamic behavior
  </Card>

  <Card title="Result types" icon="circle-check" href="/product/process/actions/custom-actions/result-types">
    All the ways to return feedback to users
  </Card>

  <Card title="Context & scope" icon="circle-info" href="/product/process/actions/custom-actions/context-scope">
    Understanding scopes and the context object
  </Card>

  <Card title="Related data invalidation" icon="arrows-rotate" href="/product/process/actions/custom-actions/related-data-invalidation">
    Refresh related data after actions
  </Card>
</CardGroup>
