Skip to content

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:

js
Nimbu.Cloud.extend(view, [scopes...], options, handler);

Parameters

ParameterTypeDescription
viewstringThe admin view to extend (e.g., 'channel.entries.list')
scopesstring[]Optional: Limit to specific channels/resources
optionsobjectExtension configuration (name, type, etc.)
handlerfunctionFunction to execute when triggered

Extension Options

js
{
  name: 'Action Name',        // Required: Display name in admin
  type: 'action' | 'view'     // Optional: Extension type (default: 'action')
}

Available Extension Views

Channel Entries

js
// 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

js
// 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

js
// 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

js
// 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

js
Nimbu.Cloud.extend('collection.list',
  { name: 'Rebuild All' },
  handler
);

Nimbu.Cloud.extend('collection.edit',
  { name: 'Import Products' },
  handler
);

Coupons

js
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:

PropertyDescriptionAvailability
req.objectThe object being acted uponDetail views only
req.params.selected_idsArray of selected IDsList views only
req.paramsAdditional form dataAll views
req.actorBackend user who triggered the actionAll views

Accessing Selected Items

In list view extensions, access selected items:

js
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:

js
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:

js
res.success('Operation completed successfully');
res.success(`Processed ${count} items`);

Error Message

Show an error notification:

js
res.error('Operation failed');
res.error('Please select at least one item');

Redirect

Redirect to another page:

js
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:

js
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):

js
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:

js
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:

js
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:

js
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:

js
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:

js
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:

js
// 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:

js
// Applies to all channel entry lists
Nimbu.Cloud.extend('channel.entries.list',
  { name: 'Global Action' },
  handler
);

Best Practices

1. Provide Clear Feedback

js
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

js
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

js
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

js
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

js
// 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

js
// 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

js
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:

  1. Navigate to the appropriate list or detail view
  2. Look for your extension in the actions menu
  3. Select items (for list views) and trigger the extension
  4. Check the notification for success/error messages
  5. Review Cloud Code logs for console output

Next Steps