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

Navigate to another page:

js
res.redirect_to('/admin/products');
res.redirect_to('/admin/orders', {
  success: 'Orders processed successfully'
});

TIP

res.redirectTo() is also available as a camelCase alias.

Send File

Send file data for download in the browser:

js
res.send(fileData, {
  filename: 'export.csv',
  type: 'text/csv',
  disposition: 'inline'
});

The fileData must be base64-encoded. Modules like CSV.to_csv(), PDF.render(), and Zip.create() return base64 by default.

OptionTypeDescription
filenamestringSuggested filename for the download
typestringMIME type (auto-detected from filename if omitted)
dispositionstring"inline" (display in browser) or "attachment" (prompt save dialog). Default: "inline"

Show Modal

Display a modal dialog with form inputs. When the user submits the modal, the same extension handler is called again with the form values in req.params:

js
res.modal({
  title: 'Update Settings',
  submit: 'Save',           // submit button label (or false to hide)
  close: 'Cancel',          // close button label (or false to hide)
  maxWidth: 500,             // optional max width in pixels (default: 600)
  blocks: [
    {
      type: 'section',
      text: { type: 'mrkdwn', text: 'Choose the **options** below.' }
    },
    { type: 'divider' },
    {
      type: 'input',
      name: 'email',
      label: { type: 'plain_text', text: 'Email' },
      hint: { type: 'plain_text', text: 'Enter the recipient email.' },
      element: { type: 'text' }
    }
  ]
});
PropertyTypeDescription
titlestringModal title
submitstring | falseSubmit button label, or false to hide
closestring | falseClose/cancel button label, or false to hide
maxWidthnumberMax width in pixels (default: 600)
blocksarrayArray of block objects

Block Types

TypePropertiesDescription
sectiontextText content (plain string or {type, text} object)
dividerHorizontal rule separator
inputname, label, hint, placeholder, elementForm input field

Text Objects

Text properties (text, label, hint, placeholder) accept either a plain string or a structured object:

js
// Plain string
{ label: 'Email Address' }

// Structured (supports markdown with type: 'mrkdwn')
{ label: { type: 'plain_text', text: 'Email Address' } }
{ text: { type: 'mrkdwn', text: 'Choose **options** below.' } }

Input Element Types

Element TypeDescriptionExtra Properties
textSingle-line text input
numberNumeric inputdecimal (boolean), negative (boolean, default: true)
hiddenHidden field
wysiwygRich text editor
selectSingle-select dropdownoptions (array or string)
multi_selectMulti-select dropdownoptions (array or string)
radio_buttonsRadio button groupoptions (array)

Select Options

Options can be an array of {value, text} objects or a string reference to a resource:

js
// Explicit options
element: {
  type: 'select',
  options: [
    { value: 'draft', text: 'Draft' },
    { value: 'published', text: 'Published' }
  ]
}

// Reference to a channel (shows entries as options)
element: {
  type: 'multi_select',
  options: 'audience'  // channel slug
}

String references: "customers", "products", or any channel slug.

Handling Modal Submissions

When the modal is submitted, the extension runs again with form data in req.params. Use this pattern to detect whether to show the modal or process the submission:

js
Nimbu.Cloud.extend('channel.entries.show', 'events',
  { name: 'Schedule Notification' },
  async (req, res) => {
    // No params yet → show the modal
    if (Object.keys(req.params).length === 0 || req.params.audience == null) {
      return res.modal({
        title: 'Schedule Notification',
        submit: 'Schedule',
        close: 'Cancel',
        blocks: [
          {
            type: 'input',
            name: 'audience',
            label: { type: 'plain_text', text: 'Audience' },
            element: { type: 'multi_select', options: 'audience' }
          }
        ]
      });
    }

    // Params present → process the submission
    // req.params.audience contains the selected values
    // ... do work ...

    res.success('Notification scheduled!');
  }
);

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 a CSV file download:

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;
    }

    const products = [];
    for (const id of selectedIds) {
      const product = await new Nimbu.Query('products').get(id);
      products.push(product);
    }

    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);

    res.send(csv, {
      filename: 'products.csv',
      type: 'text/csv',
      disposition: 'attachment'
    });
  }
);

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');
  }
);

Example 8: Download Invoice PDF

Generate and download a PDF from an order:

js
const PDF = require('pdf');

Nimbu.Cloud.extend(
  'order.show',
  { name: 'Download Invoice' },
  async (req, res) => {
    const order = req.object;
    const html = `<html><body>
      <h1>Invoice #${order.id}</h1>
      <p>Total: €${order.get('total')}</p>
    </body></html>`;

    const pdf = PDF.render(html);

    res.send(pdf, {
      filename: `invoice-${order.id}.pdf`,
      type: 'application/pdf',
      disposition: 'inline'
    });
  }
);

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