
Basic structure
Here’s what a code-based action looks like:agent.customizeCollection('companies', collection =>
collection.addAction('Mark as Live', {
scope: 'Single',
description: 'Mark the company as live',
form: [
{
type: 'Date',
label: 'Live date',
isRequired: true,
},
{
type: 'String',
label: 'Notes',
widget: 'TextArea',
},
],
execute: async (context, resultBuilder) => {
// Access form values
const { 'Live date': liveDate, Notes: notes } = context.formValues;
// Access selected record
const company = await context.getRecord(['id', 'name']);
// Perform business logic
await markCompanyAsLive(company.id, liveDate, notes);
// Return result
return resultBuilder.success(`${company.name} is now live!`);
},
}),
);
include ForestAdminDatasourceCustomizer::Decorators::Action
include ForestAdminDatasourceCustomizer::Decorators::Action::Types
forest_agent.customize_collection('companies') do |collection|
collection.add_action(
'Mark as Live',
BaseAction.new(
scope: ActionScope::SINGLE,
description: 'Mark the company as live',
form: [
{
type: FieldType::DATE,
label: 'Live date',
is_required: true
},
{
type: FieldType::STRING,
label: 'Notes',
widget: 'TextArea'
}
]
) do |context, result_builder|
# Access form values
live_date = context.form_values['Live date']
notes = context.form_values['Notes']
# Access selected record
company = context.get_record(['id', 'name'])
# Perform business logic
mark_company_as_live(company['id'], live_date, notes)
# Return result
result_builder.success("#{company['name']} is now live!")
end
)
end
@create_agent.collection :companies do |collection|
collection.action 'Mark as Live', scope: :single do
description 'Mark the company as live'
form do
field :live_date, type: :date, is_required: true
field :notes, type: :string, widget: 'TextArea'
end
execute do
# Access form values
live_date = form_value(:live_date)
notes = form_value(:notes)
# Access selected record
company = record(['id', 'name'])
# Perform business logic
mark_company_as_live(company['id'], live_date, notes)
# Return result
success "#{company['name']} is now live!"
end
end
end
Common examples
Send email
collection.addAction('Send Welcome Email', {
scope: 'Single',
form: [
{ type: 'String', label: 'Subject', isRequired: true },
{ type: 'String', label: 'Message', widget: 'TextArea', isRequired: true },
],
execute: async (context, resultBuilder) => {
const user = await context.getRecord(['email', 'name']);
const { Subject, Message } = context.formValues;
try {
await sendEmail({
to: user.email,
subject: Subject,
body: Message,
});
return resultBuilder.success(`Email sent to ${user.email}`);
} catch (error) {
return resultBuilder.error(`Failed to send email: ${error.message}`);
}
},
});
collection.add_action(
'Send Welcome Email',
BaseAction.new(
scope: ActionScope::SINGLE,
form: [
{ type: FieldType::STRING, label: 'Subject', is_required: true },
{ type: FieldType::STRING, label: 'Message', widget: 'TextArea', is_required: true }
]
) do |context, result_builder|
user = context.get_record(['email', 'name'])
subject = context.form_values['Subject']
message = context.form_values['Message']
begin
send_email(
to: user['email'],
subject: subject,
body: message
)
result_builder.success("Email sent to #{user['email']}")
rescue => error
result_builder.error("Failed to send email: #{error.message}")
end
end
)
Charge credit card
collection.addAction('Charge Credit Card', {
scope: 'Single',
form: [
{ type: 'Number', label: 'Amount', isRequired: true },
{ type: 'String', label: 'Description', isRequired: true },
],
execute: async (context, resultBuilder) => {
const customer = await context.getRecord(['stripeId', 'email']);
const { Amount, Description } = context.formValues;
try {
const charge = await stripe.charges.create({
amount: Amount * 100,
currency: 'usd',
customer: customer.stripeId,
description: Description,
});
return resultBuilder.success('Charge successful', {
html: `
<p>Charged $${Amount} to ${customer.email}</p>
<p>Transaction ID: ${charge.id}</p>
`,
});
} catch (error) {
return resultBuilder.error(`Charge failed: ${error.message}`);
}
},
});
collection.add_action(
'Charge Credit Card',
BaseAction.new(
scope: ActionScope::SINGLE,
form: [
{ type: FieldType::NUMBER, label: 'Amount', is_required: true },
{ type: FieldType::STRING, label: 'Description', is_required: true }
]
) do |context, result_builder|
customer = context.get_record(['stripeId', 'email'])
amount = context.form_values['Amount']
description = context.form_values['Description']
begin
charge = Stripe::Charge.create(
amount: (amount * 100).to_i,
currency: 'usd',
customer: customer['stripeId'],
description: description
)
result_builder.success(
'Charge successful',
html: "<p>Charged $#{amount} to #{customer['email']}</p>
<p>Transaction ID: #{charge.id}</p>"
)
rescue => error
result_builder.error("Charge failed: #{error.message}")
end
end
)
Bulk status update
collection.addAction('Update Status', {
scope: 'Bulk',
form: [
{
type: 'Enum',
label: 'New Status',
enumValues: ['pending', 'approved', 'rejected'],
isRequired: true,
},
],
execute: async (context, resultBuilder) => {
const orders = await context.getRecords(['id']);
const newStatus = context.formValues['New Status'];
const ids = orders.map(o => o.id);
await Order.update({ status: newStatus }, { where: { id: ids } });
return resultBuilder.success(`Updated ${ids.length} orders to ${newStatus}`);
},
});
collection.add_action(
'Update Status',
BaseAction.new(
scope: ActionScope::BULK,
form: [
{
type: FieldType::ENUM,
label: 'New Status',
enum_values: ['pending', 'approved', 'rejected'],
is_required: true
}
]
) do |context, result_builder|
orders = context.get_records(['id'])
new_status = context.form_values['New Status']
ids = orders.map { |o| o['id'] }
Order.where(id: ids).update_all(status: new_status)
result_builder.success("Updated #{ids.length} orders to #{new_status}")
end
)
collection.action 'Update Status', scope: :bulk do
form do
field :new_status, type: :enum, enum_values: ['pending', 'approved', 'rejected'], is_required: true
end
execute do
orders = records(['id'])
new_status = form_value(:new_status)
ids = orders.map { |o| o['id'] }
Order.where(id: ids).update_all(status: new_status)
success "Updated #{ids.length} orders to #{new_status}"
end
end
Generate report
collection.addAction('Generate Report', {
scope: 'Global',
generateFile: true,
form: [
{ type: 'DateOnly', label: 'Start Date', isRequired: true },
{ type: 'DateOnly', label: 'End Date', isRequired: true },
],
execute: async (context, resultBuilder) => {
const { 'Start Date': startDate, 'End Date': endDate } = context.formValues;
const data = await fetchReportData(startDate, endDate);
const pdf = await generatePDF(data);
return resultBuilder.file(
pdf,
`report-${startDate}-${endDate}.pdf`,
'application/pdf'
);
},
});
collection.add_action(
'Generate Report',
BaseAction.new(
scope: ActionScope::GLOBAL,
is_generate_file: true,
form: [
{ type: FieldType::DATE_ONLY, label: 'Start Date', is_required: true },
{ type: FieldType::DATE_ONLY, label: 'End Date', is_required: true }
]
) do |context, result_builder|
start_date = context.form_values['Start Date']
end_date = context.form_values['End Date']
data = fetch_report_data(start_date, end_date)
pdf = generate_pdf(data)
result_builder.file(
content: pdf,
name: "report-#{start_date}-#{end_date}.pdf",
mime_type: 'application/pdf'
)
end
)
Learn more
Forms
Form fields, validation, and dynamic behavior
Result types
All the ways to return feedback to users
Context & scope
Understanding scopes and the context object
Related data invalidation
Refresh related data after actions