API cheatsheet
| Legacy agent | New agent |
|---|---|
collection(name, { actions: [...] }) | agent.customizeCollection(name, c => c.addAction('Name', ...)) |
name | First argument of addAction |
type: 'single' | 'bulk' | 'global' | scope: 'Single' | 'Bulk' | 'Global' |
fields: [...] (form definition) | form: [...] |
download: true | generateFile: true |
| Express handler at custom route | execute: (context, resultBuilder) => ... |
req.body.data.attributes.values | context.formValues |
req.body.data.attributes.ids | context.getRecordIds() |
res.send({ success: '...' }) | resultBuilder.success('...') |
res.send({ error: '...' }) | resultBuilder.error('...') |
res.redirect(url) | resultBuilder.redirectTo(url) |
Before (Node.js, forest-express-sequelize)
After (Node.js, @forestadmin/agent)
Before (Ruby, forest-rails)
After (Ruby, forest_admin_rails)
Customizations live insideForestAdminRails::CreateAgent.customize in app/lib/forest_admin_rails/create_agent.rb:
Result types
The new agent supports the same result types as v1, plus a few new ones, all returned viaresultBuilder:
| Result | API | Use case |
|---|---|---|
| Success message | resultBuilder.success(message) | Confirmation toast |
| Error message | resultBuilder.error(message) | Failure toast |
| Redirect | resultBuilder.redirectTo(url) | Open external URL |
| File download | resultBuilder.file(buffer, filename, mimeType) | Generate and download a file |
| HTML response | resultBuilder.webhookSuccess(message, html) | Show custom HTML |
Form fields
Forms in the new agent are typed and support dynamic behavior more cleanly:Approval workflows
If your legacy action used the approval system, the configuration moves from the action declaration to Project Settings → Roles. The action code itself doesn’t need changes. Forest’s UI handles the approval gating around yourexecute function.
Bulk and global actions
Legacy type | New scope |
|---|---|
'single' | 'Single' |
'bulk' | 'Bulk' |
'global' | 'Global' |
context.getRecordIds() to retrieve every selected record’s primary key, or context.getRecords(fields) to fetch the full records.
Common conversions
Action that returns a file download
Action that returns a file download
v1 used
download: true and the route returned a file via res. v2 returns the file via resultBuilder.file(buffer, filename, mimeType) and sets generateFile: true in the action declaration.Action with dynamic form fields
Action with dynamic form fields
v1 supported
change hooks on form fields. v2 uses the if and defaultValue properties, which receive a context and the current form values. See Action forms.Action with custom permission logic
Action with custom permission logic
v1 used
Liana.ensureAuthenticated middleware and custom permission checks. v2 uses the standard role and team permission system configured in the UI. For dynamic checks (e.g. only the assigned rep can run an action), use action visibility conditions or check inside execute and return resultBuilder.error(...).Action that triggers a webhook
Action that triggers a webhook
Direct port:
execute calls your webhook URL the same way the v1 route handler did. The axios.post(...) (or Net::HTTP.post(...)) line is unchanged.Migration checklist
List every Smart Action in your project
Pull from your
forest/ (Node.js) or app/services/forest_liana/actions/ (Ruby) directory.For each action, port the declaration
Replace
Liana.collection(...).actions = [...] with agent.customizeCollection(...).addAction(...).Port the form definition
Convert each
field to a form entry. Update widget names (camelCase → PascalCase: 'text area' → 'TextArea').Port the execute logic
Move the route handler body into the
execute function. Replace req.body.data.attributes.values with context.formValues.Next step
Migrate Smart Fields
Convert computed fields to the new
addField API with explicit dependencies.