Create and manage Smart Actions

Please be sure of your agent type and version and pick the right documentation accordingly.

This is the documentation of the forest-express-sequelize and forest-express-mongoose Node.js agents that will soon reach end-of-support.

forest-express-sequelize v9 and forest-express-mongoose v9 are replaced by @forestadmin/agent v1.

Please check your agent type and version and read on or switch to the right documentation.

Create and manage Smart Actions

What is a Smart Action?

Sooner or later, you will need to perform actions on your data that are specific to your business. Moderating comments, generating an invoice, logging into a customer’s account or banning a user are exactly the kind of important tasks to unlock in order to manage your day-to-day operations.

On our Live Demo example, our companies collection has many examples of Smart Action. The simplest one is Mark as live.

If you're looking for information on native actions (CRUD), check out this page.

Creating a Smart action

In order to create a Smart action, you will first need to declare it in your code for a specific collection. Here we declare a Mark as Live Smart action for the companies collection.

/forest/companies.js
const { collection } = require('forest-express-sequelize');

collection('companies', {
  actions: [
    {
      name: 'Mark as Live',
    },
  ],
});

After declaring it, your Smart action will appear in the Smart actions tab within your collection settings.

A Smart action is displayed in the UI only if:

  • it is set as "visible" (see screenshot below) AND

  • in non-development environments, the user's role must grant the "trigger" permission

You must make the action visible there if you wish users to be able to see it.

It will then show in the actions dropdown button:

At this point, the Smart Action does nothing, because no route in your Admin backend handles the API call yet.

The Smart Action behavior is implemented separately from the declaration.

In the following example, we've implemented the Mark as live Smart Action, which simply changes a company's status to live.

/routes/companies.js
const { PermissionMiddlewareCreator } = require('forest-express-sequelize');
const permissionMiddlewareCreator = new PermissionMiddlewareCreator('companies');

...

router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
  const recordsGetter = new RecordsGetter(companies, request.user, request.query);

  return recordsGetter.getIdsFromRequest(req)
    .then(companyIds => companies.update({ status: 'live' }, { where: { id: companyIds }}))
    .then(() => res.send({ success: 'Company is now live!' }));
});

...

module.exports = router;

You must make sure that all your Smart Actions routes are configured with the Smart Action middleware: permissionMiddlewareCreator.smartAction(). This is mandatory to ensure that all features built on top of Smart Actions work as expected (permissions, approval workflows,...).

What's happening under the hood?

When you trigger the Smart Action from the UI, your browser will make an API call: POST /forest/actions/mark-as-live.

If you want to customize the API call, check the list of available options.

The payload of the HTTP request is based on a JSON API document. The data.attributes.ids key allows you to retrieve easily the selected records from the UI. The data.attributes.values key contains all the values of your input fields (handling input values). Other properties of data.attributes are used to manage the select all behavior.

payload example
{
  "data": {
    "attributes": {
      "ids": ["1985"],
      "values": {},
      "collection_name": "companies",
      ...
    },
    "type": "custom-action-requests"
  }
}

Should you want not to use the RecordsGetter and use request attributes directly instead, be very careful about edge cases (related data view, etc).

Available Smart Action options

Here is the list of available options to customize your Smart Action:

NameTypeDescription

name

string

Label of the action displayed in Forest Admin.

type

string

(optional) Type of the action. Can be bulk, global or single. Default is bulk.

fields

array of objects

(optional) Check the handling input values section.

download

boolean

(optional) If true, the action triggers a file download in the Browser. Default is false

endpoint

string

(optional) Set the API route to call when clicking on the Smart Action. Default is '/forest/actions/name-of-the-action-dasherized'

httpMethod

string

(optional) Set the HTTP method to use when clicking on the Smart Action. Default is POST.

Want to go further with Smart Actions? Read the next page to discover how to make your Smart Actions even more powerful with Forms!

Available Smart Action properties

req.user

The JWT Data Token contains all the details of the requesting user. On any authenticated request to your Admin Backend, you can access them with the variable req.user.

req.user content example

{
  "id": "172",
  "email": "angelicabengtsson@doha2019.com",
  "firstName": "Angelica",
  "lastName": "Bengtsson",
  "team": "Pole Vault",
  "role": "Manager",
  "tags": [{ key: "country", value: "Canada" }],
  "renderingId": "4998",
  "iat": 1569913709,
  "exp": 1571123309
}

req.body

You can find important information in the body of the request.

This is particularly useful to find the context in which an action was performed via a relationship.

{
  data: {
    attributes: {
      collection_name: 'users', //collection on which the action has been triggered
      values: {},
      ids: [Array], //IDs of selected records
      parent_collection_name: 'companies', //Parent collection name
      parent_collection_id: '1', //Parent collection id
      parent_association_name: 'users', //Name of the association
      all_records: false,
      all_records_subset_query: {},
      all_records_ids_excluded: [],
      smart_action_id: 'users-reset-password'
    },
    type: 'custom-action-requests'
  }
}

Customizing response

Default success notification

Returning a 204 status code to the HTTP request of the Smart Action shows the default notification message in the browser.

On our Live Demo example, if our Smart Action Mark as Live route is implemented like this:

/routes/companies.js
...

router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
  // ...
  res.status(204).send();
});

...

We will see a success message in the browser:

Custom success notification

If we return a 200 status code with an object { success: '...' } as the payload like this…

/routes/companies.js
...

router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
  // ...
  res.send({ success: 'Company is now live!' });
});

...

… the success notification will look like this:

Custom error notification

Finally, returning a 400 status code allows you to return errors properly.

/routes/companies.js
...

router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
  // ...
  res.status(400).send({ error: 'The company was already live!' });
});

...

Custom HTML response

You can also return a HTML page as a response to give more feedback to the admin user who has triggered your Smart Action. To do this, you just need to return a 200 status code with an object { html: '...' }.

On our Live Demo example, we’ve created a Charge credit card Smart Action on the Collection customersthat returns a custom HTML response.

/forest/companies.js
const { collection } = require('forest-express-sequelize');

collection('customers', {
  actions: [
    {
      name: 'Charge credit card',
      type: 'single',
      fields: [
        {
          field: 'amount',
          isRequired: true,
          description:
            'The amount (USD) to charge the credit card. Example: 42.50',
          type: 'Number',
        },
        {
          field: 'description',
          isRequired: true,
          description:
            'Explain the reason why you want to charge manually the customer here',
          type: 'String',
        },
      ],
    },
  ],
});
/routes/customers.js
...
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

router.post('/actions/charge-credit-card', permissionMiddlewareCreator.smartAction(), (req, res) => {
  let customerId = req.body.data.attributes.ids[0];
  let amount = req.body.data.attributes.values.amount * 100;
  let description = req.body.data.attributes.values.description;

  return customers
    .findByPk(customerId)
    .then((customer) => {
      return stripe.charges.create({
        amount: amount,
        currency: 'usd',
        customer: customer.stripe_id,
        description: description
      });
    })
    .then((response) => {
      res.send({
        html: `
        <p class="c-clr-1-4 l-mt l-mb">\$${response.amount / 100} USD has been successfully charged.</p>
        <strong class="c-form__label--read c-clr-1-2">Credit card</strong>
        <p class="c-clr-1-4 l-mb">**** **** **** ${response.source.last4}</p>
        <strong class="c-form__label--read c-clr-1-2">Expire</strong>
        <p class="c-clr-1-4 l-mb">${response.source.exp_month}/${response.source.exp_year}</p>
        <strong class="c-form__label--read c-clr-1-2">Card type</strong>
        <p class="c-clr-1-4 l-mb">${response.source.brand}</p>
        <strong class="c-form__label--read c-clr-1-2">Country</strong>
        <p class="c-clr-1-4 l-mb">${response.source.country}</p>
        `
      });
    });
});

...

module.exports = router;

You can either respond with an HTML page in case of error. The user will be able to go back to his smart action's form by using the cross icon at the top right of the panel.

/forest/companies.js
const { collection } = require('forest-express-sequelize');

collection('customers', {
  actions: [
    {
      name: 'Charge credit card',
      type: 'single',
      fields: [
        {
          field: 'amount',
          isRequired: true,
          description:
            'The amount (USD) to charge the credit card. Example: 42.50',
          type: 'Number',
        },
        {
          field: 'description',
          isRequired: true,
          description:
            'Explain the reason why you want to charge manually the customer here',
          type: 'String',
        },
      ],
    },
  ],
});
/routes/customers.js
...
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

router.post('/actions/charge-credit-card', permissionMiddlewareCreator.smartAction(), (req, res) => {
  let customerId = req.body.data.attributes.ids[0];
  let amount = req.body.data.attributes.values.amount * 100;
  let description = req.body.data.attributes.values.description;

  return customers
    .findByPk(customerId)
    .then((customer) => {
      return stripe.charges.create({
        amount: amount,
        currency: 'usd',
        customer: customer.stripe_id,
        description: description
      });
    })
    .then((response) => {
      res.status(400).send({
        html: `
        <p class="c-clr-1-4 l-mt l-mb">$${response.amount / 100} USD has not been charged.</p>
        <strong class="c-form__label--read c-clr-1-2">Credit card</strong>
        <p class="c-clr-1-4 l-mb">**** **** **** ${response.source.last4}</p>
        <strong class="c-form__label--read c-clr-1-2">Reason</strong>
        <p class="c-clr-1-4 l-mb">You can not charge this credit card. The card is marked as blocked</p>
        `
      });
    });
});

...

module.exports = router;

Setting up a webhook

After a smart action you can set up a HTTP (or HTTPS) callback - a webhook - to forward information to other applications. To set up a webhook all you have to do is to add a webhookobject in the response of your action.

response.send({
  webhook: {
    // This is the object that will be used to fire http calls.
    url: 'http://my-company-name', // The url of the company providing the service.
    method: 'POST', // The method you would like to use (typically a POST).
    headers: {}, // You can add some headers if needed (you can remove it).
    body: {
      // A body to send to the url (only JSON supported).
      adminToken: 'your-admin-token',
    },
  },
});

Webhooks are commonly used to perform smaller requests and tasks, like sending emails or impersonating a user.

Another interesting use of this is automating SSO authentication into your external apps.

Downloading a file

On our Live Demo, the collection customers has a Smart Action Generate invoice. In this use case, we want to download the generated PDF invoice after clicking on the action. To indicate a Smart Action returns something to download, you have to enable the option download.

/forest/customers.js
const { collection } = require('forest-express-sequelize');

collection('customers', {
  actions: [
    {
      name: 'Generate invoice',
      download: true, // If true, the action triggers a file download in the Browser.
    },
  ],
});
/routes/customers.js
...

router.post('/actions/generate-invoice', permissionMiddlewareCreator.smartAction(),
  (req, res) => {
    let options = {
      root: __dirname + '/../public/',
      dotfiles: 'deny',
      headers: {
        'Access-Control-Expose-Headers': 'Content-Disposition',
        'Content-Disposition': 'attachment; filename="invoice-2342.pdf"'
      }
    };

    let fileName = 'invoice-2342.pdf';
    res.sendFile(fileName, options, (error) => {
      if (error) { next(error); }
    });
  });

...

module.exports = router;

Want to upload your files to Amazon S3? Check out this this Woodshop tutorial.

If you want to create an action accessible from the details or the summary view of a record involving related data, this section may interest you.

In the example below, the “Add new transaction” action (1) is accessible from the summary view. This action creates a new transaction and automatically refresh the “Emitted transactions” related data section (2) to see the new transaction.

Below is the sample code. We use faker to generate random data in our example. Remember to install it if you wish to use it (npm install faker).

/forest/companies.js
const { collection } = require('forest-express-sequelize');

collection('companies', {
  actions: [
    {
      name: 'Add new transaction',
      description: 'Name of the company who will receive the transaction.',
      fields: [
        {
          field: 'Beneficiary company',
          description: 'Name of the company who will receive the transaction.',
          reference: 'companies.id',
        },
        {
          field: 'Amount',
          type: 'Number',
        },
      ],
    },
  ],
});
/routes/companies.js
...
const faker = require('faker');

router.post('/actions/add-new-transaction', permissionMiddlewareCreator.smartAction(),
  (req, res) => {
    let emitterCompanyId = req.body.data.attributes.ids[0]
    let beneficiaryCompanyId = req.body.data.attributes.values['Beneficiary company']
    let amount = req.body.data.attributes.values['Amount']

    return transactions
      .create({
        emitter_company_id: emitterCompanyId,
        beneficiary_company_id: beneficiaryCompanyId,
        beneficiary_iban: faker.finance.iban(),
        emitter_iban: faker.finance.iban(),
        vat_amount: faker.finance.amount(500, 10000, 0),
        fee_amount: faker.finance.amount(500, 10000, 0),
        status: ['to_validate', 'validated', 'rejected'].sample,
        note: faker.lorem.sentences(),
        amount: amount,
        emitter_bic: faker.finance.bic(),
        beneficiary_bic: faker.finance.bic()
      })
      .then(() => {
	    // the code below automatically refresh the related data
    	// 'emitted_transactions' on the Companies' Summary View
    	// after submitting the Smart action form.
        res.send({
          success: 'New transaction emitted',
          refresh: { relationships: ['emitted_transactions'] },
        });
      });
});

Redirecting to a different page on success

To streamline your operation workflow, it could make sense to redirect to another page after a Smart action was successfully executed. It is possible using the redirectTo property. The redirection works both for internal (*.forestadmin.com pages) and external links.

External links will open in a new tab.

Here's a working example for both cases:

/forest/models.js
const { collection } = require('forest-express-sequelize');

collection('models', {
  actions: [
    {
      name: 'Return and track',
    },
    {
      name: 'Show some activity',
    },
  ],
});
/routes/models.js
...

// External redirection
router.post('/actions/return-and-track', permissionMiddlewareCreator.smartAction(),
  (req, res) => {
    res.send({
      success: 'Return initiated successfully.',
      redirectTo: 'https://www.royalmail.com/portal/rm/track?trackNumber=ZW924750388GB',
    });
  }
);

// Internal redirection
router.post('/actions/show-some-activity', permissionMiddlewareCreator.smartAction(),
  (req, res) => {
    res.send({
      success: 'Navigated to the activity view.',
      redirectTo: '/MyProject/MyEnvironment/MyTeam/data/20/index/record/20/108/activity',
    });
  }
);

...

module.exports = router;

Your external links must use the http or https protocol.

Enable/Disable a Smart Action according to the state of a record

Sometimes, your Smart Action only makes sense depending on the state of your records. On our Live Demo, it does not make any sense to enable the Mark as Live Smart Action on the companies collection if the company is already live, right?

In the collection settings, you can configure the UI options of your Smart Actions.

Restrict a smart action to specific roles

When using Forest Admin collaboratively with clear roles defined it becomes relevant to restrict a smart action only to a select few. This functionality is accessible through Smart Actions Permissions in the Role section of your Project Settings. Learn more about roles.

Require approval for a Smart action

Critical actions for your business may need approval before being processed.

Set up your approval workflow

To add an additional layer of security over a smart action, head over to the Roles tab of your projects settings. From there, you'll be able to select Trigger with approval for that smart action. Note that this must be set for each role.

Review approval requests

Actions requiring approval will be available in the Collaboration menu (3) in the “Approvals” section:

  • “Requested” for all incoming requests (yours to approve or not)

  • “To Review” (4) for requests you need to review

  • “History” for all past requests.

In “To Review”, you will be able to approve or reject the request (5) with an optional message (6) for more details.

Review past approval requests

All past approval requests - made by you or other approvers - in the History tab (1).

You can export your approval requests history from this tab using the top right button (2).

You can get more details on a specific action by clicking on it:

Want to go further with Smart Actions? Read the next page to discover how to make your Smart Actions even more powerful with Forms!

Last updated