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 the previous page.

Creating a Smart Action

To declare a Smart Action for the collection companies, add it to the corresponding file in your /forest folder:

SQL
Mongodb
/forest/companies.js
const { collection } = require('forest-express-sequelize');
collection('companies', {
actions: [{
name: 'Mark as Live'
}],
});
/forest/companies.js
const { collection } = require('forest-express-mongoose');
collection('companies', {
actions: [{
name: 'Mark as Live'
}],
});

After declaring it, your Smart Action will appear 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.

To implement the Smart Action behavior, head over to the /routes folder.

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

SQL
Mongodb
/routes/companies.js
...
router.post('/actions/mark-as-live',
(req, res) => {
let companyId = req.body.data.attributes.ids[0];
return models.companies
.update({ status: 'live' }, { where: { id: companyId }})
.then(() => {
res.send({ success: 'Company is now live!' });
});
});
...
module.exports = router;
/routes/companies.js
...
router.post('/actions/mark-as-live', Liana.ensureAuthenticated, (req, res) => {
let companyId = req.body.data.attributes.ids[0];
return Company
.findOneAndUpdate({ _id: companyId }, { $set: { status: 'live' }})
.then(() => res.send({ success: 'Company is now live!' }));
});
...
module.exports = router;

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/mask-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).

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

Available Smart Action options

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

Name

Type

Description

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.

Opening a form to ask input values

Very often, you will need to ask user inputs before triggering the logic behind a Smart Action. For example, you might want to specify a reason if you want to block a user account. Or set the amount to charge a user’s credit card.

SQL
Mongodb

On our Live Demo example, we’ve defined 4 input fields on the Smart Action Upload Legal Docs on the collection companies.

/forest/companies.js
const { collection } = require('forest-express-sequelize');
collection('companies', {
actions: [{
name: 'Upload Legal Docs',
type: 'single',
fields: [{
field: 'Certificate of Incorporation',
description: 'The legal document relating to the formation of a company or corporation.',
type: 'File',
isRequired: true
}, {
field: 'Proof of address',
description: '(Electricity, Gas, Water, Internet, Landline & Mobile Phone Invoice / Payment Schedule) no older than 3 months of the legal representative of your company',
type: 'File',
isRequired: true
}, {
field: 'Company bank statement',
description: 'PDF including company name as well as IBAN',
type: 'File',
isRequired: true
}, {
field: 'Valid proof of ID',
description: 'ID card or passport if the document has been issued in the EU, EFTA, or EEA / ID card or passport + resident permit or driving licence if the document has been issued outside the EU, EFTA, or EEA of the legal representative of your company',
type: 'File',
isRequired: true
}],
});
/routes/companies.js
...
router.post('/actions/upload-legal-docs',
(req, res) => {
// Get the current company id
let companyId = req.body.data.attributes.ids[0];
// Get the values of the input fields entered by the admin user.
let attrs = req.body.data.attributes.values;
let certificate_of_incorporation = attrs['Certificate of Incorporation'];
let proof_of_address = attrs['Proof of address'];
let company_bank_statement = attrs['Company bank statement'];
let passport_id = attrs['Valid proof of id'];
// The business logic of the Smart Action. We use the function
// UploadLegalDoc to upload them to our S3 repository. You can see the full
// implementation on our Forest Live Demo repository on Github.
return P.all([
uploadLegalDoc(companyId, certificate_of_incorporation, 'certificate_of_incorporation_id'),
uploadLegalDoc(companyId, proof_of_address, 'proof_of_address_id'),
uploadLegalDoc(companyId, company_bank_statement,'bank_statement_id'),
uploadLegalDoc(companyId, passport_id, 'passport_id'),
])
.then(() => {
// Once the upload is finished, send a success message to the admin user in the UI.
res.send({ success: 'Legal documents are successfully uploaded.' });
});
});
...
module.exports = router;

On our Live Demo example, we’ve defined 4 input fields on the Smart Action Upload Legal Docs on the collection companies.

/forest/companies.js
const { collection } = require('forest-express-mongoose');
collection('companies', {
actions: [{
name: 'Upload Legal Docs',
type: 'single',
fields: [{
field: 'Certificate of Incorporation',
description: 'The legal document relating to the formation of a company or corporation.',
type: 'File',
isRequired: true
}, {
field: 'Proof of address',
description: '(Electricity, Gas, Water, Internet, Landline & Mobile Phone Invoice / Payment Schedule) no older than 3 months of the legal representative of your company',
type: 'File',
isRequired: true
}, {
field: 'Company bank statement',
description: 'PDF including company name as well as IBAN',
type: 'File',
isRequired: true
}, {
field: 'Valid proof of ID',
description: 'ID card or passport if the document has been issued in the EU, EFTA, or EEA / ID card or passport + resident permit or driving licence if the document has been issued outside the EU, EFTA, or EEA of the legal representative of your company',
type: 'File',
isRequired: true
}],
});
/routes/companies.js
...
router.post('/actions/upload-legal-docs',
(req, res) => {
// Get the current company id
let companyId = req.body.data.attributes.ids[0];
// Get the values of the input fields entered by the admin user.
let attrs = req.body.data.attributes.values;
let certificate_of_incorporation = attrs['Certificate of Incorporation'];
let proof_of_address = attrs['Proof of address'];
let company_bank_statement = attrs['Company bank statement'];
let passport_id = attrs['Valid proof of id'];
// The business logic of the Smart Action. We use the function
// UploadLegalDoc to upload them to our S3 repository. You can see the full
// implementation on our Forest Live Demo repository on Github.
return P.all([
uploadLegalDoc(companyId, certificate_of_incorporation, 'certificate_of_incorporation_id'),
uploadLegalDoc(companyId, proof_of_address, 'proof_of_address_id'),
uploadLegalDoc(companyId, company_bank_statement,'bank_statement_id'),
uploadLegalDoc(companyId, passport_id, 'passport_id'),
])
.then(() => {
// Once the upload is finished, send a success message to the admin user in the UI.
res.send({ success: 'Legal documents are successfully uploaded.' });
});
});
...
module.exports = router;

Handling input values

Here is the list of available options to customize your input form.

Name

Type

Description

field

string

Label of the input field.

type

string or array

Type of your field.

  • string: Boolean, Date, Dateonly, Enum, File, Number, String

  • array: ['Enum'], ['Number'], ['String']

enums

array of strings

(optional) Required only for the Enum type. This is where you list all the possible values for your input field.

description

string

(optional) Add a description for your admin users to help them fill correctly your form

isRequired

boolean

(optional) If true, your input field will be set as required in the browser. Default is false.

Prefill a form with default values

Forest Admin allows you to set default values to your form. In this example, we will prefill the form with data coming from the record itself (1), with just a few extra lines of code.

SQL
Mongodb
/forest/customers.js
const { collection } = require('forest-express-sequelize');
const models = require('../models/');
const _ = require('lodash');
collection('customers', {
actions: [{
name: 'Generate invoice',
download: true
}, {
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'
}, {
// we added a field to show the full potential of prefilled values in this example
field: 'stripe_id',
isRequired: true,
type: 'String'
}],
// In values you define the appropriate prefilled value of each field
values: (context) => {
return {
amount: 4520,
stripe_id: context.stripe_id,
};
},
}],
...
});
/forest/customers.js
const { collection } = require('forest-express-mongoose');
const models = require('../models/');
const _ = require('lodash');
collection('Customer', {
actions: [{
name: 'Generate invoice',
download: true
}, {
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'
}, {
// we added a field to show the full potential of prefilled values in this example
field: 'stripe_id',
isRequired: true,
type: 'String'
}],
// In values you define the appropriate prefilled value of each field
values: (context) => {
return {
amount: 4520,
stripe_id: context.stripe_id,
};
},
}],
...
});

Values function is available for single type action only.

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', (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', (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', Liana.ensureAuthenticated, (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.

SQL
Mongodb
/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', (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 models.customers
.findById(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 successfuly 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;
/forest/companies.js
const { collection } = require('forest-express-mongoose');
collection('Customer', {
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', (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 Customer
.findById(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 successfuly 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;

Downloading a file

SQL
Mongodb

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', Liana.ensureAuthenticated,
(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;

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-mongoose');
collection('Customer', {
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', Liana.ensureAuthenticated,
(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;

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.

SQL
Mongodb

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', Liana.ensureAuthenticated,
(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 models.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'] },
});
});
});

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-mongoose');
collection('Company', {
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: 'Company'
},{
field: 'Amount',
type: 'Number'
}]
}],
});
/routes/companies.js
const express = require('express');
const router = express.Router();
const Liana = require('forest-express-mongoose');
const Transaction = require('../models/transactions');
const faker = require('faker');
// ...
router.post('/actions/add-new-transaction', Liana.ensureAuthenticated,
(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 Transaction
.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:

SQL
Mongodb
/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',
(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',
(req, res) => {
res.send({
success: 'Navigated to the activity view.',
redirectTo: '/1/data/20/index/record/20/108/activity',
});
}
);
...
module.exports = router;
/forest/models.js
const { collection } = require('forest-express-mongoose');
collection('models', {
actions: [{
name: 'Initiate return and display tracking',
}, {
name: 'Show some activity',
}],
});
/routes/models.js
...
// External redirection
router.post('/actions/return-and-track', Liana.ensureAuthenticated,
(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', Liana.ensureAuthenticated,
(req, res) => {
res.send({
success: 'Navigated to the activity view.',
redirectTo: '/1/data/20/index/record/20/108/activity/preview',
});
}
);
...
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 users

When using Forest Admin with several teams and when you have clear roles defined it becomes relevant to restrict a smart action only to a few collaborators. This option is accessible through the Edit layout mode (1) in the Smart actions’ section (3) of the collection’s options (2), in a dropdown menu (4).

Require approval for a Smart action

This feature requires an Enterprise plan.

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

Set up your approval workflow

The “This smart action requires an approval” option (1) is accessible in the Smart action section of your collection’s settings. You can select approvers among your users (2). Once this option is activated, a warning pop up indicates that the approval workflow is need to perform this action.

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: