Create and manage Smart Actions
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
.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.SQL
Mongodb
Rails
Django
Laravel
/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'
}],
});
/lib/forest_liana/collections/company.rb
class Forest::Company
include ForestLiana::Collection
collection :Company
action 'Mark as Live'
end
app/forest/companies.py
from django_forest.utils.collection import Collection
from app.models import Company
class CompanyForest(Collection):
def load(self):
self.actions = [{
'name': 'Mark as Live'
}]
Collection.register(CompanyForest, Company)
Ensure the file app/forest/__init__.py exists and contains the import of the previous defined class :
app/forest/__init__.py
from app.forest.companies import CompanyForest
app/Models/Company.php
<?php
namespace App\Models;
use ForestAdmin\LaravelForestAdmin\Services\Concerns\ForestCollection;
use ForestAdmin\LaravelForestAdmin\Services\SmartFeatures\SmartAction;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
* Class Company
*/
class Company extends Model
{
use HasFactory;
use ForestCollection;
/**
* @return SmartAction
*/
public function markAsLive(): SmartAction
{
return $this->smartAction('single', '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
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
.SQL
Mongodb
Rails
Django
Laravel
/routes/companies.js
...
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;
/routes/companies.js
...
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.updateMany({ _id: { $in: companyIds }}, { $set: { status: 'live' }}))
.then(() => res.send({ success: 'Company is now live!' }));
});
...
module.exports = router;
The route declaration takes place in
config/routes.rb
./config/routes.rb
Rails.application.routes.draw do
# MUST be declared before the mount ForestLiana::Engine.
namespace :forest do
post '/actions/mark-as-live' => 'companies#mark_as_live'
end
mount ForestLiana::Engine => '/forest'
end
The business logic in this Smart Action is extremely simple. We only update here the attribute
status
of the companies to the value live
:/app/controllers/forest/companies_controller.rb
class Forest::CompaniesController < ForestLiana::SmartActionsController
def mark_as_live
company_id = ForestLiana::ResourcesGetter.get_ids_from_request(params, forest_user).first
Company.update(company_id, status: 'live')
head :no_content
end
end
Note that Forest Admin takes care of the authentication thanks to the
ForestLiana::SmartActionsController
parent class controller.You may have to add CORS headers to enable the domain
app.forestadmin.com
to trigger API call on your Application URL, which is on a different domain name (e.g. localhost:3000).Make sure your project
urls.py
file include you app urls with the forest
prefix.urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('forest', include('app.urls')),
path('forest', include('django_forest.urls')),
path('admin/', admin.site.urls),
]
The route declaration takes place in
app/urls.py
.app/urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from . import views
app_name = 'app'
urlpatterns = [
path('/actions/mark-as-live', csrf_exempt(views.MarkAsLiveView.as_view()), name='mark-as-live'),
]
The business logic in this Smart Action is extremely simple. We only update here the attribute
status
of the companies to the value live
:app/views.py
from django.http import JsonResponse
from django_forest.utils.views.action import ActionView
class MarkAsLiveView(ActionView):
def post(self, request, *args, **kwargs):
params = request.GET.dict()
body = self.get_body(request.body)
ids = self.get_ids_from_request(request, self.Model)
return JsonResponse({'success': 'live!'})
Note that Forest Admin takes care of the authentication thanks to the
ActionView
parent class view.You may have to add CORS headers to enable the domain
app.forestadmin.com
to trigger API call on your Application URL, which is on a different domain name (e.g. localhost:8000).The route declaration takes place in
routes/web.php
.routes/web.php
<?php
use App\Http\Controllers\CompaniesController;
use App\Http\Controllers\OrdersController;
use Illuminate\Support\Facades\Route;
Route::post('forest/smart-actions/company_mark-as-live', [CompaniesController::class, 'markAsLive']);
The business logic in this Smart Action is extremely simple. We only update here the attribute
status
of the companies to the value live
:app/Http/Controllers/CompaniesController.php
<?php
namespace App\Http\Controllers;
use App\Models\Company;
use ForestAdmin\LaravelForestAdmin\Http\Controllers\ForestController;
use Illuminate\Http\JsonResponse;
/**
* Class CompaniesController
*/
class CompaniesController extends ForestController
{
/**
* @return JsonResponse
*/
public function markAsLive(): JsonResponse
{
$id = request()->input('data.attributes.ids')[0];
$company = Company::findOrFail($id);
$company->status = 'live';
$company->save();
return response()->noContent();
}
}
When you trigger the Smart Action from the UI, your browser will make an API call:
POST /forest/actions/mark-as-live
.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).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 | |
fields | array of objects | |
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!
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": "[email protected]",
"firstName": "Angelica",
"lastName": "Bengtsson",
"team": "Pole Vault",
"role": "Manager",
"tags": [{ key: "country", value: "Canada" }],
"renderingId": "4998",
"iat": 1569913709,
"exp": 1571123309
}
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'
}
}
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:

If we return a 200 status code with an object
{ success: '...' }
as the payload like this…SQL
Mongodb
Rails
Django
Laravel
/routes/companies.js
...
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
// ...
res.send({ success: 'Company is now live!' });
});
...
/routes/companies.js
...
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
// ...
res.send({ success: 'Company is now live!' });
});
...
class Forest::CompaniesController < ForestLiana::SmartActionsController
def mark_as_live
# ...
render json: { success: 'Company is now live!' }
end
end
app/views.py
from django.http import JsonResponse
from django_forest.utils.views.action import ActionView
class MarkAsLiveView(ActionView):
def post(self, request, *args, **kwargs):
return JsonResponse({'success': 'Company is now live!'})
app/Http/Controllers/CompaniesController.php
class CompaniesController extends ForestController
{
/**
* @return JsonResponse
*/
public function markAsLive(): JsonResponse
{
# ....
return response()->json(['success' => "Company is now live !"]);
}
}
… the success notification will look like this:

Finally, returning a 400 status code allows you to return errors properly.
SQL
Mongodb
Rails
Django
Laravel
/routes/companies.js
...
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
// ...
res.status(400).send({ error: 'The company was already live!' });
});
...
/routes/companies.js
...
router.post('/actions/mark-as-live', permissionMiddlewareCreator.smartAction(), (req, res) => {
// ...
res.status(400).send({ error: 'The company was already live!' });
});
...
/app/controllers/forest/companies_controller.rb
class Forest::CompaniesController < ForestLiana::SmartActionsController
def mark_as_live
# ...
render status: 400, json: { error: 'The company was already live!' }
end
end
app/views.py
from django.http import JsonResponse
from django_forest.utils.views.action import ActionView
class MarkAsLiveView(ActionView):
def post(self, request, *args, **kwargs):
return JsonResponse({'error': 'The company was already live!'}, status=400)
app/Http/Controllers/CompaniesController.php
class CompaniesController extends ForestController
{
/**
* @return JsonResponse
*/
public function markAsLive(): JsonResponse
{
# ....
return response()->json(['error' => "The company was already live!"], 400);
}
}
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 customers
that returns a custom HTML response.SQL
Mongodb
Rails
Django
Laravel
/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;
/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 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;
/lib/forest_liana/collections/customer.rb
class Forest::Customer
include ForestLiana::Collection
collection :Customer
action 'Charge credit card', type: 'single', fields: [{
field: 'amount',
is_required: true,
description: 'The amount (USD) to charge the credit card. Example: 42.50',
type: 'Number'
}, {
field: 'description',
is_required: true,
description: 'Explain the reason why you want to charge manually the customer here',
type: 'String'
}]
end
/config/routes.rb
Rails.application.routes.draw do
# MUST be declared before the mount ForestLiana::Engine.
namespace :forest do
post '/actions/charge-credit-card' => 'customers#charge_credit_card'
end
mount ForestLiana::Engine => '/forest'
end
/app/controllers/forest/customers_controller.rb
class Forest::CustomersController < ForestLiana::SmartActionsController
def charge_credit_card
customer_id = ForestLiana::ResourcesGetter.get_ids_from_request(params).first
amount = params.dig('data', 'attributes', 'values', 'amount').to_i
description = params.dig('data', 'attributes', 'values', 'description')
customer = Customer.find(customer_id)
response = Stripe::Charge.create(
amount: amount * 100,
currency: 'usd',
customer: customer.stripe_id,
description: description
)
render json: { html: <<EOF
<p class="c-clr-1-4 l-mt l-mb">$#{response.amount / 100.0} 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>
EOF
}
end
end
app/forest/customer.py
from django_forest.utils.collection import Collection
from app.models import Company
class CompanyForest(Collection):
def load(self):
self.actions = [{
'name': 'Charge credit card',
'fields': [
{
'field': 'amount',
'description': 'The amount (USD) to charge the credit card. Example: 42.50',
'isRequired': True,
'type': 'Number'
},
{
'field': 'description',
'description': 'Explain the reason why you want to charge manually the customer here',
'isRequired': True,
'type': 'String'
},
],
}]
Collection.register(CompanyForest, Company)
app/urls.py
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from . import views
app_name = 'app'
urlpatterns = [
path('/actions/charge-credit-card', csrf_exempt(views.ChargeCreditCardView.as_view()), name='charge-credit-card'),
]
app/views.py
import stripe
from django.http import JsonResponse
from django_forest.utils.views.action import ActionView
from .models import Customer
class ChargeCreditCardView(ActionView):
def post(self, request, *args, **kwargs):
params = request.GET.dict()
body = self.get_body(request.body)
ids = self.get_ids_from_request(request, self.Model)
amount = body['data']['attributes']['values']['amount'] * 100
description = body['data']['attributes']['values']['description']
customer = Customer.object.get(pk=ids[0])
stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
response = stripe.Charge.create(
amount=amount,
currency='usd',
customer=customer.stripe_id,
description=description,
)
data = f'''
<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>
'''
return JsonResponse({'html': data}, safe=False)
app/Models/Customer.php
<?php
namespace App\Models;
use ForestAdmin\LaravelForestAdmin\Services\Concerns\ForestCollection;
use ForestAdmin\LaravelForestAdmin\Services\SmartFeatures\SmartAction;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Class Customer
*/
class Customer extends Model
{
use HasFactory, ForestCollection;
/**
* @return SmartAction
*/
public