Actions can return different types of results to provide feedback to users. Use the result builder to control what happens after an action executes.
Default behavior
If you don’t return anything and no exception is thrown, Forest displays a generic success notification.
execute: async (context, resultBuilder) => {
// Perform your logic
// No return = generic success message
}
Success notification
Display a custom success message.
return resultBuilder.success('Company is now live!');
Error notification
Display an error message when something goes wrong.
if (!isValid) {
return resultBuilder.error('The company was already live!');
}
Always handle errors gracefully and return meaningful error messages to help users understand what went wrong.
HTML result
Return rich formatted content displayed in a side panel. Perfect for showing detailed operation results.
return resultBuilder.success('Charge successful', {
html: `
<p class="c-clr-1-4 l-mt l-mb">
$${amount} 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">**** **** **** ${last4}</p>
<strong class="c-form__label--read c-clr-1-2">Transaction ID</strong>
<p class="c-clr-1-4 l-mb">${transactionId}</p>
`,
});
HTML with error
You can also return HTML content with an error:
return resultBuilder.error('Charge failed', {
html: `
<p class="c-clr-1-4 l-mt l-mb">
$${amount} USD has not been charged.
</p>
<strong class="c-form__label--read c-clr-1-2">Reason</strong>
<p class="c-clr-1-4 l-mb">
The credit card is marked as blocked.
</p>
`,
});
File download
Generate and download files (PDFs, CSVs, Excel, etc.).
Actions that generate files must set generateFile: true in their configuration. This flag prevents using other result types (notifications, HTML) in the same action.
collection.addAction('Download report', {
scope: 'Global',
generateFile: true, // Required for file downloads
execute: async (context, resultBuilder) => {
// From a string
return resultBuilder.file(
'Report content here',
'report.txt',
'text/plain'
);
// From a Buffer
const buffer = Buffer.from('Report content');
return resultBuilder.file(buffer, 'report.txt', 'text/plain');
// From a stream
const stream = fs.createReadStream('path/to/file.pdf');
return resultBuilder.file(stream, 'report.pdf', 'application/pdf');
},
});
Common MIME types
| File type | MIME type |
|---|
| PDF | application/pdf |
| CSV | text/csv |
| Excel (xlsx) | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
| Excel (xls) | application/vnd.ms-excel |
| JSON | application/json |
| ZIP | application/zip |
| Plain text | text/plain |
| HTML | text/html |
Example: Generate CSV
collection.addAction('Export to CSV', {
scope: 'Bulk',
generateFile: true,
execute: async (context, resultBuilder) => {
const records = await context.getRecords(['id', 'name', 'email']);
// Generate CSV content
const header = 'ID,Name,Email\n';
const rows = records.map(r => `${r.id},"${r.name}","${r.email}"`).join('\n');
const csv = header + rows;
return resultBuilder.file(csv, 'export.csv', 'text/csv');
},
});
Example: Generate PDF
collection.addAction('Generate invoice', {
scope: 'Single',
generateFile: true,
execute: async (context, resultBuilder) => {
const order = await context.getRecord(['id', 'total', 'customer:name']);
// Generate PDF (using a library like pdfkit)
const pdf = await generateInvoicePDF(order);
return resultBuilder.file(
pdf,
`invoice-${order.id}.pdf`,
'application/pdf'
);
},
});
Redirect
Redirect users to another page after the action executes. Works for both internal Forest pages and external URLs.
Internal redirect
Redirect to another page within Forest:
return resultBuilder.redirectTo(
'/MyProject/MyEnvironment/MyTeam/data/20/index/record/20/108/activity'
);
External redirect
Redirect to an external URL:
return resultBuilder.redirectTo(
'https://www.example.com/tracking?id=ZW924750388GB'
);
Example: Redirect to created record
collection.addAction('Create and view', {
scope: 'Global',
execute: async (context, resultBuilder) => {
// Create a new record
const newRecord = await createRecord(context.formValues);
// Redirect to the new record's detail page
return resultBuilder.redirectTo(
`/MyProject/Production/data/companies/index/record/companies/${newRecord.id}/details`
);
},
});
Webhook
Trigger an HTTP callback from the user’s browser. Useful for logging into third-party applications or triggering operations on the user’s behalf.
Webhooks are triggered from the user’s browser and are subject to CORS restrictions. Make sure the target server accepts requests from Forest domains.
return resultBuilder.webhook(
'https://api.example.com/callback', // URL
'POST', // Method
{ Authorization: 'Bearer token' }, // Headers
{ userId: 123, action: 'approve' } // Body (JSON)
);
Example: Single sign-on
collection.addAction('Login to external tool', {
scope: 'Single',
execute: async (context, resultBuilder) => {
const user = await context.getRecord(['email', 'externalId']);
// Generate a temporary token
const token = await generateSSOToken(user.externalId);
// Trigger login in the user's browser
return resultBuilder.webhook(
'https://external-tool.com/sso/login',
'POST',
{},
{ token, email: user.email }
);
},
});
Example: Trigger background job
collection.addAction('Process data', {
scope: 'Bulk',
execute: async (context, resultBuilder) => {
const ids = await context.getRecordIds();
// Trigger a background job with user context
return resultBuilder.webhook(
'https://api.mycompany.com/jobs/process',
'POST',
{ 'X-API-Key': process.env.API_KEY },
{
recordIds: ids,
userId: context.caller.id,
triggeredAt: new Date().toISOString()
}
);
},
});
Combining results with operations
Success with HTML details
execute: async (context, resultBuilder) => {
const result = await performComplexOperation();
if (result.success) {
return resultBuilder.success('Operation completed', {
html: `
<h3>Summary</h3>
<p>Processed ${result.count} items</p>
<ul>
${result.items.map(i => `<li>${i}</li>`).join('')}
</ul>
`,
});
} else {
return resultBuilder.error('Operation failed', {
html: `<p>Error: ${result.error}</p>`,
});
}
}
Conditional redirect
execute: async (context, resultBuilder) => {
const order = await context.getRecord(['status', 'id']);
if (order.status === 'pending') {
// Update and redirect to details
await updateOrder(order.id, { status: 'approved' });
return resultBuilder.redirectTo(`/orders/${order.id}`);
} else {
// Already processed
return resultBuilder.error('Order was already processed');
}
}