Extensions
Extensions allow you to add custom actions and functionality to the Nimbu admin interface. Use them to create bulk operations, custom workflows, or specialized tools for your team.
What are Extensions?
Extensions are custom actions that appear in the Nimbu admin interface and execute Cloud Code when triggered. They enable you to:
- Add bulk operation buttons to list views
- Create custom actions on detail pages
- Build specialized workflows for your team
- Automate repetitive administrative tasks
- Integrate admin actions with external services
Defining Extensions
Use Nimbu.Cloud.extend() to create an extension:
Nimbu.Cloud.extend(view, [scopes...], options, handler);Parameters
| Parameter | Type | Description |
|---|---|---|
view | string | The admin view to extend (e.g., 'channel.entries.list') |
scopes | string[] | Optional: Limit to specific channels/resources |
options | object | Extension configuration (name, type, etc.) |
handler | function | Function to execute when triggered |
Extension Options
{
name: 'Action Name', // Required: Display name in admin
type: 'action' | 'view' // Optional: Extension type (default: 'action')
}Available Extension Views
Channel Entries
// List view - Bulk actions on multiple entries
Nimbu.Cloud.extend('channel.entries.list', 'blog',
{ name: 'Export Selected' },
async (req, res) => {
// req.params.selected_ids contains selected entry IDs
}
);
// Detail view - Actions on single entry
Nimbu.Cloud.extend('channel.entries.show', 'blog',
{ name: 'Publish to Social Media' },
async (req, res) => {
// req.object is the blog entry
}
);Customers
// Customer list actions
Nimbu.Cloud.extend('customer.list',
{ name: 'Send Newsletter' },
async (req, res) => {
// Bulk customer action
}
);
// Customer detail actions
Nimbu.Cloud.extend('customer.show',
{ name: 'Reset Password' },
async (req, res) => {
// Single customer action
}
);Products
// Product list actions
Nimbu.Cloud.extend('product.list',
{ name: 'Update Prices' },
async (req, res) => {
// Bulk product action
}
);
// Product detail actions
Nimbu.Cloud.extend('product.show',
{ name: 'Sync to External System' },
async (req, res) => {
// Single product action
}
);Orders
// Order list actions
Nimbu.Cloud.extend('order.list',
{ name: 'Export Orders' },
async (req, res) => {
// Bulk order action
}
);
// Order detail actions
Nimbu.Cloud.extend('order.show',
{ name: 'Resend Invoice' },
async (req, res) => {
// Single order action
}
);Collections
Nimbu.Cloud.extend('collection.list',
{ name: 'Rebuild All' },
handler
);
Nimbu.Cloud.extend('collection.edit',
{ name: 'Import Products' },
handler
);Coupons
Nimbu.Cloud.extend('coupon.list',
{ name: 'Deactivate Expired' },
handler
);
Nimbu.Cloud.extend('coupon.edit',
{ name: 'Duplicate Coupon' },
handler
);Request Object
The request object provides context about the extension trigger:
| Property | Description | Availability |
|---|---|---|
req.object | The object being acted upon | Detail views only |
req.params.selected_ids | Array of selected IDs | List views only |
req.params | Additional form data | All views |
req.actor | Backend user who triggered the action | All views |
Accessing Selected Items
In list view extensions, access selected items:
Nimbu.Cloud.extend('channel.entries.list', 'products',
{ name: 'Export Selected' },
async (req, res) => {
const selectedIds = req.params.selected_ids;
for (const id of selectedIds) {
const product = await new Nimbu.Query('products').get(id);
// Process each product...
}
res.success(`Processed ${selectedIds.length} products`);
}
);Accessing Current Object
In detail view extensions, access the current object:
Nimbu.Cloud.extend('order.show',
{ name: 'Send Tracking Email' },
async (req, res) => {
const order = req.object;
const trackingNumber = order.get('tracking_number');
const customerEmail = order.get('customer_email');
// Send email...
res.success('Tracking email sent');
}
);Response Methods
Use the response object to provide feedback:
Success Message
Show a success notification:
res.success('Operation completed successfully');
res.success(`Processed ${count} items`);Error Message
Show an error notification:
res.error('Operation failed');
res.error('Please select at least one item');Redirect
Redirect to another page:
res.redirect('/admin/products');
res.redirect_to('/admin/orders', {
success: 'Orders processed successfully'
});Real-World Examples
Example 1: Send Welcome Email (Bulk Action)
Send welcome emails to selected customers:
const Mail = require('mail');
Nimbu.Cloud.extend(
'customer.list',
{ name: 'Send Welcome Email' },
async (req, res) => {
const selectedIds = req.params.selected_ids;
if (!selectedIds || selectedIds.length === 0) {
res.error('Please select at least one customer');
return;
}
let successCount = 0;
for (const id of selectedIds) {
try {
const customer = await new Nimbu.Query('customers').get(id);
await Mail.send({
to: customer.get('email'),
template: 'welcome_email',
variables: {
name: customer.get('name')
}
});
successCount++;
} catch (error) {
console.error(`Failed to send email to customer ${id}:`, error.message);
}
}
res.success(`Welcome email sent to ${successCount} customers`);
}
);Example 2: Manual Job Trigger
Trigger a background job manually from the admin (from theme-hr-gids):
Nimbu.Cloud.extend(
'channel.entries.list',
'hr_wijzers',
{ name: 'Check for Newsletter' },
async (req, res) => {
// Trigger the newsletter job immediately
Nimbu.Cloud.schedule('hr_wijzers_check_new', {}, {});
res.success('Newsletter job scheduled successfully');
}
);Example 3: Export to CSV
Export selected products to CSV:
const Csv = require('csv');
Nimbu.Cloud.extend(
'product.list',
{ name: 'Export to CSV' },
async (req, res) => {
const selectedIds = req.params.selected_ids;
if (!selectedIds || selectedIds.length === 0) {
res.error('Please select products to export');
return;
}
// Fetch products
const products = [];
for (const id of selectedIds) {
const product = await new Nimbu.Query('products').get(id);
products.push(product);
}
// Generate CSV
const data = [
['ID', 'Name', 'Price', 'Stock'],
...products.map(p => [
p.id,
p.get('name'),
p.get('price'),
p.get('stock')
])
];
const csv = Csv.to_csv(data);
// For file download, you'd typically save and redirect
// or use a route to serve the file
res.success(`Exported ${products.length} products`);
}
);Example 4: Sync to External Service
Sync a product to an external inventory system:
const HTTP = require('http');
const SiteVariables = require('site_variables');
Nimbu.Cloud.extend(
'product.show',
{ name: 'Sync to Inventory System' },
async (req, res) => {
const product = req.object;
try {
const apiKey = SiteVariables.get('INVENTORY_API_KEY');
await HTTP.post('https://api.inventory.com/products', {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sku: product.get('sku'),
name: product.get('name'),
stock: product.get('stock_quantity')
})
});
res.success('Product synced successfully');
} catch (error) {
console.error('Sync failed:', error.message);
res.error('Failed to sync product');
}
}
);Example 5: Duplicate Order
Create a copy of an order:
Nimbu.Cloud.extend(
'order.show',
{ name: 'Duplicate Order' },
async (req, res) => {
const originalOrder = req.object;
// Create new order
const newOrder = new Nimbu.Object('orders');
newOrder.set('customer', originalOrder.get('customer'));
newOrder.set('items', originalOrder.get('items'));
newOrder.set('shipping_address', originalOrder.get('shipping_address'));
newOrder.set('status', 'draft');
newOrder.set('notes', 'Duplicated from order ' + originalOrder.id);
await newOrder.save();
res.redirect_to(`/admin/orders/${newOrder.id}`, {
success: 'Order duplicated successfully'
});
}
);Example 6: Bulk Price Update
Update prices for selected products:
Nimbu.Cloud.extend(
'product.list',
{ name: 'Apply 10% Discount' },
async (req, res) => {
const selectedIds = req.params.selected_ids;
if (!selectedIds || selectedIds.length === 0) {
res.error('Please select products');
return;
}
let updatedCount = 0;
for (const id of selectedIds) {
try {
const product = await new Nimbu.Query('products').get(id);
const currentPrice = product.get('price');
const newPrice = currentPrice * 0.9; // 10% discount
product.set('price', newPrice);
product.set('original_price', currentPrice);
await product.save();
updatedCount++;
} catch (error) {
console.error(`Failed to update product ${id}:`, error.message);
}
}
res.success(`Updated ${updatedCount} products with 10% discount`);
}
);Example 7: Generate Report
Generate and send a report about selected items:
const Mail = require('mail');
Nimbu.Cloud.extend(
'order.list',
{ name: 'Email Report' },
async (req, res) => {
const selectedIds = req.params.selected_ids;
const adminEmail = req.actor.email;
// Fetch orders
const orders = [];
let totalRevenue = 0;
for (const id of selectedIds) {
const order = await new Nimbu.Query('orders').get(id);
orders.push(order);
totalRevenue += order.get('total') || 0;
}
// Send report email
await Mail.send({
to: adminEmail,
subject: 'Order Report',
html: `
<h1>Order Report</h1>
<p>Orders: ${orders.length}</p>
<p>Total Revenue: €${totalRevenue.toFixed(2)}</p>
`
});
res.success('Report sent to your email');
}
);Scoping Extensions
Channel-Specific Extensions
Limit extensions to specific channels:
// Only for blog channel
Nimbu.Cloud.extend('channel.entries.list', 'blog',
{ name: 'Publish to Medium' },
handler
);
// For multiple channels
Nimbu.Cloud.extend('channel.entries.list', 'blog', 'news',
{ name: 'Feature Article' },
handler
);Global Extensions
Omit the scope to apply to all:
// Applies to all channel entry lists
Nimbu.Cloud.extend('channel.entries.list',
{ name: 'Global Action' },
handler
);Best Practices
1. Provide Clear Feedback
Nimbu.Cloud.extend('product.list',
{ name: 'Update Stock' },
async (req, res) => {
const count = req.params.selected_ids.length;
// Process items...
// Clear success message
res.success(`Successfully updated stock for ${count} products`);
}
);2. Validate Selection
Nimbu.Cloud.extend('customer.list',
{ name: 'Send Email' },
async (req, res) => {
const selectedIds = req.params.selected_ids;
if (!selectedIds || selectedIds.length === 0) {
res.error('Please select at least one customer');
return;
}
// Process...
}
);3. Handle Errors Gracefully
Nimbu.Cloud.extend('order.show',
{ name: 'Process Refund' },
async (req, res) => {
try {
await processRefund(req.object);
res.success('Refund processed successfully');
} catch (error) {
console.error('Refund failed:', error.message);
res.error('Failed to process refund. Please try again.');
}
}
);4. Log Important Actions
Nimbu.Cloud.extend('product.list',
{ name: 'Delete Selected' },
async (req, res) => {
const selectedIds = req.params.selected_ids;
console.log('[Extension] Delete action triggered by:', req.actor.email);
console.log('[Extension] Products to delete:', selectedIds.length);
// Process deletion...
console.log('[Extension] Deletion completed');
res.success(`Deleted ${selectedIds.length} products`);
}
);5. Keep Actions Fast
// Good: Queue heavy work
Nimbu.Cloud.extend('order.list',
{ name: 'Generate Reports' },
(req, res) => {
// Queue a background job for heavy processing
Nimbu.Cloud.schedule('generate_reports', {
orderIds: req.params.selected_ids
}, {});
res.success('Report generation started');
}
);
// Avoid: Heavy processing in extension
Nimbu.Cloud.extend('order.list',
{ name: 'Process All' },
async (req, res) => {
// This might timeout:
for (const id of req.params.selected_ids) {
await heavyProcessing(id); // Slow!
}
}
);Common Patterns
Confirmation Before Action
// Note: Confirmation must be handled in UI
// Extension executes when user confirms
Nimbu.Cloud.extend('product.list',
{ name: 'Delete Selected Products' },
async (req, res) => {
// This runs after user confirms in UI
const selectedIds = req.params.selected_ids;
for (const id of selectedIds) {
const product = await new Nimbu.Query('products').get(id);
await product.destroy();
}
res.success(`Deleted ${selectedIds.length} products`);
}
);Batch Processing
Nimbu.Cloud.extend('customer.list',
{ name: 'Add to VIP Group' },
async (req, res) => {
const selectedIds = req.params.selected_ids;
const vipRole = await getRole('vip_customers');
// Add all customers to role in batch
for (const customerId of selectedIds) {
vipRole.getCustomers().add(
new Nimbu.Customer({ id: customerId })
);
}
await vipRole.save();
res.success(`Added ${selectedIds.length} customers to VIP group`);
}
);Testing Extensions
Extensions appear in the Nimbu admin interface:
- Navigate to the appropriate list or detail view
- Look for your extension in the actions menu
- Select items (for list views) and trigger the extension
- Check the notification for success/error messages
- Review Cloud Code logs for console output
Next Steps
- Learn about Background Jobs - Schedule automated tasks
- Explore Callbacks - React to data changes
- Review Cloud Functions - Callable server-side functions
- Check Routes - Create custom HTTP endpoints