Skip to content

Callbacks

Callbacks allow you to execute custom code before or after data operations. Use them to validate data, enforce business rules, or trigger side effects when objects are created, updated, or deleted.

What are Callbacks?

Callbacks are functions that run automatically when:

  • A new object is created
  • An existing object is updated
  • An object is deleted

They work regardless of where the change originates - whether from your theme, the admin interface, or the API.

Callback Types

Before Callbacks

Run before an operation completes. Use them for:

  • Validation - Block invalid data from being saved
  • Data transformation - Modify values before saving
  • Business rule enforcement - Ensure invariants are maintained
js
Nimbu.Cloud.before('created', 'customers', (req, res) => {
  // Validate or transform data
  // Call res.error() to block the operation
  // Call res.success() to allow it
});

After Callbacks

Run after an operation completes. Use them for:

  • Side effects - Send emails, webhooks, or notifications
  • External integrations - Sync data to third-party services
  • Derived data updates - Update related objects
js
Nimbu.Cloud.after('created', 'orders', (req, res) => {
  // Trigger side effects
  // Operation has already completed
});

Supported Events

Channel Entries

js
// Before events
Nimbu.Cloud.before('channel.entries.created', handler);
Nimbu.Cloud.before('channel.entries.updated', handler);
Nimbu.Cloud.before('channel.entries.deleted', handler);

// After events
Nimbu.Cloud.after('channel.entries.created', handler);
Nimbu.Cloud.after('channel.entries.updated', handler);
Nimbu.Cloud.after('channel.entries.deleted', handler);

Customers

js
// Before events
Nimbu.Cloud.before('customer.created', handler);
Nimbu.Cloud.before('customer.updated', handler);
Nimbu.Cloud.before('customer.deleted', handler);

// After events
Nimbu.Cloud.after('customer.created', handler);
Nimbu.Cloud.after('customer.updated', handler);
Nimbu.Cloud.after('customer.deleted', handler);

Products

js
// Before events
Nimbu.Cloud.before('product.created', handler);
Nimbu.Cloud.before('product.updated', handler);
Nimbu.Cloud.before('product.deleted', handler);

// After events
Nimbu.Cloud.after('product.created', handler);
Nimbu.Cloud.after('product.updated', handler);
Nimbu.Cloud.after('product.deleted', handler);

Coupons

js
// Before/after events
Nimbu.Cloud.before('coupon.created', handler);
Nimbu.Cloud.after('coupon.created', handler);
// ... updated, deleted

Devices

js
// Before/after events
Nimbu.Cloud.before('device.created', handler);
Nimbu.Cloud.after('device.created', handler);
// ... updated, deleted

Orders (After only)

Orders only support after callbacks:

js
Nimbu.Cloud.after('order.created', handler);
Nimbu.Cloud.after('order.updated', handler);
Nimbu.Cloud.after('order.canceled', handler);
Nimbu.Cloud.after('order.fulfilled', handler);
Nimbu.Cloud.after('order.paid', handler);
Nimbu.Cloud.after('order.reopened', handler);
Nimbu.Cloud.after('order.attachments.ready', handler);
Nimbu.Cloud.after('order.attachments.expired', handler);

Scoping to Channels

For channel entries, you can scope callbacks to specific channels:

js
// Apply to specific channel
Nimbu.Cloud.before('channel.entries.created', 'blog', (req, res) => {
  // Only runs for blog entries
});

// Apply to multiple channels
Nimbu.Cloud.before(
  'channel.entries.created',
  'channel.entries.updated',
  'newsletter',
  (req, res) => {
    // Runs for both created and updated events on newsletter channel
  }
);

// Apply to all channels
Nimbu.Cloud.before('channel.entries.created', (req, res) => {
  // Runs for all channel entry creations
});

Request Object

The request object provides information about the operation:

PropertyDescriptionExample
req.objectThe Nimbu.Object being operated onreq.object.get('email')
req.actorCustomer or user who triggered the actionreq.actor (can be null)
req.userBackend user (if triggered from admin)req.user
req.customerAlias for req.actor when it's a customerreq.customer
req.changesObject with field changes (update/delete only)req.changes.email
req.lastUpdatedAtPrevious update timestamp (update only)Date object

Accessing Object Data

js
Nimbu.Cloud.before('created', 'customers', (req, res) => {
  const email = req.object.get('email');
  const name = req.object.get('name');

  console.log('Creating customer:', email);
});

Detecting Changes

In updated and deleted callbacks, req.changes shows what changed:

js
Nimbu.Cloud.before('channel.entries.updated', 'products', (req, res) => {
  if (req.changes.price) {
    const [oldPrice, newPrice] = req.changes.price;
    console.log(`Price changed from ${oldPrice} to ${newPrice}`);
  }
});

Response Object

Use the response object to control the operation:

Success

Allow the operation to proceed:

js
res.success();

Error (Validation)

Block the operation with a validation error:

js
// Field-specific error
res.error('email', 'Email address is invalid');

// General error
res.error('Operation not allowed');

Real-World Examples

Example 1: Email Validation

Validate email addresses before creating customers:

js
Nimbu.Cloud.before('customer.created', (req, res) => {
  const email = req.object.get('email');

  if (!email || !email.includes('@')) {
    res.error('email', 'Please provide a valid email address');
    return;
  }

  res.success();
});

Example 2: Newsletter Sync

Sync newsletter subscriptions to an external service (real example from theme-dynamo):

js
const HTTP = require('http');

Nimbu.Cloud.before(
  'channel.entries.created',
  'channel.entries.updated',
  'newsletter',
  (req, res) => {
    const email = req.object.get('email');
    const apiUrl = `https://www.ymlp.com/api/Contacts.Add?Key=XXX&Username=XXX&Email=${email}&GroupID=300`;

    console.log('Syncing newsletter subscription:', email);

    HTTP.post(apiUrl);
    res.success();
  }
);

Example 3: Auto-Generate Order Number

Automatically generate order numbers after order creation:

js
Nimbu.Cloud.after('order.created', async (req, res) => {
  const order = req.object;
  const orderNumber = `ORD-${order.id.slice(-8).toUpperCase()}`;

  order.set('order_number', orderNumber);
  await order.save();

  console.log('Generated order number:', orderNumber);
  res.success();
});

Example 4: Prevent Deletion of Active Products

Block deletion of products that are still in active orders:

js
Nimbu.Cloud.before('product.deleted', async (req, res) => {
  const product = req.object;

  // Check if product is in any orders
  const orderQuery = new Nimbu.Query('orders');
  orderQuery.equalTo('items.product_id', product.id);
  orderQuery.equalTo('status', 'active');

  const activeOrders = await orderQuery.count();

  if (activeOrders > 0) {
    res.error('Cannot delete product that is in active orders');
    return;
  }

  res.success();
});

Example 5: Send Welcome Email

Send a welcome email when a new customer signs up:

js
const Mail = require('mail');

Nimbu.Cloud.after('customer.created', async (req, res) => {
  const customer = req.object;

  await Mail.send({
    to: customer.get('email'),
    template: 'welcome_email',
    variables: {
      name: customer.get('name'),
      email: customer.get('email')
    }
  });

  console.log('Welcome email sent to:', customer.get('email'));
  res.success();
});

Example 6: Track Price Changes

Log when product prices change:

js
Nimbu.Cloud.after('product.updated', (req, res) => {
  if (req.changes.price) {
    const [oldPrice, newPrice] = req.changes.price;
    const product = req.object;

    console.log(`Price updated for ${product.get('name')}:`, {
      old: oldPrice,
      new: newPrice,
      changedBy: req.actor ? req.actor.get('email') : 'system'
    });
  }

  res.success();
});

Example 7: Auto-Populate Slug

Automatically generate URL-friendly slugs from titles:

js
Nimbu.Cloud.before('channel.entries.created', 'blog', (req, res) => {
  const title = req.object.get('title');

  if (title && !req.object.get('slug')) {
    const slug = title
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-|-$/g, '');

    req.object.set('slug', slug);
  }

  res.success();
});

Example 8: Inventory Check

Prevent product deletion if inventory exists:

js
Nimbu.Cloud.before('product.deleted', async (req, res) => {
  const product = req.object;
  const inventory = product.get('inventory_quantity') || 0;

  if (inventory > 0) {
    res.error(`Cannot delete product with ${inventory} items in stock`);
    return;
  }

  res.success();
});

Working with Processors

Callbacks have access to special processors for certain operations:

Image Resizing

Resize images during upload:

js
Nimbu.Cloud.before('channel.entries.created', 'gallery', async (req, res) => {
  const imageField = 'main_image';

  // Resize image to 800x600
  await req.processors.resizeImage(imageField, 800, 600);

  res.success();
});

Best Practices

1. Keep Callbacks Fast

Callbacks should complete quickly (< 5 seconds). For long-running operations, use background jobs:

js
// Good: Queue a job for heavy work
Nimbu.Cloud.after('order.created', (req, res) => {
  Nimbu.Cloud.schedule('process_order', {
    orderId: req.object.id
  }, {});

  res.success();
});

// Avoid: Heavy processing in callback
Nimbu.Cloud.after('order.created', async (req, res) => {
  // This might timeout:
  for (const item of heavyProcessing()) {
    await slowOperation(item);
  }
});

2. Use Before for Validation, After for Side Effects

js
// Good: Validation in before
Nimbu.Cloud.before('customer.created', (req, res) => {
  if (!isValidEmail(req.object.get('email'))) {
    res.error('email', 'Invalid email');
    return;
  }
  res.success();
});

// Good: Side effects in after
Nimbu.Cloud.after('customer.created', (req, res) => {
  sendWelcomeEmail(req.object);
  res.success();
});

3. Always Call res.success() or res.error()

js
// Good: Always respond
Nimbu.Cloud.before('created', 'orders', (req, res) => {
  if (someCondition) {
    res.error('Invalid order');
    return; // Important: stop execution
  }

  res.success();
});

// Avoid: Missing response
Nimbu.Cloud.before('created', 'orders', (req, res) => {
  if (someCondition) {
    // Forgot to call res.error()!
  }
  // Forgot to call res.success()!
});

4. Handle Errors Gracefully

js
// Good: Proper error handling
Nimbu.Cloud.after('created', 'orders', async (req, res) => {
  try {
    await sendWebhook(req.object);
    res.success();
  } catch (error) {
    console.error('Webhook failed:', error.message);
    // Still call success - don't block the operation
    res.success();
  }
});

5. Avoid Infinite Loops

Be careful when modifying objects in callbacks:

js
// Dangerous: Can cause infinite loop
Nimbu.Cloud.after('customer.updated', async (req, res) => {
  req.object.set('last_modified', new Date());
  await req.object.save(); // This triggers another update!
  res.success();
});

// Better: Check if already set
Nimbu.Cloud.after('customer.updated', async (req, res) => {
  if (!req.changes.last_modified) {
    req.object.set('last_modified', new Date());
    await req.object.save();
  }
  res.success();
});

6. Log Important Actions

js
Nimbu.Cloud.before('customer.deleted', (req, res) => {
  console.log('Customer deletion requested:', {
    id: req.object.id,
    email: req.object.get('email'),
    requestedBy: req.actor ? req.actor.get('email') : 'system'
  });

  res.success();
});

Debugging Callbacks

View Logs

Check the Cloud Code logs in the Nimbu admin interface to see:

  • When callbacks are triggered
  • Console output
  • Errors that occurred

Add Debug Logging

js
Nimbu.Cloud.before('channel.entries.updated', 'products', (req, res) => {
  console.log('[Product Update] Starting validation');
  console.log('Changed fields:', Object.keys(req.changes));
  console.log('Actor:', req.actor ? req.actor.get('email') : 'none');

  // Validation logic...

  console.log('[Product Update] Validation passed');
  res.success();
});

Test with Different Scenarios

Test your callbacks by:

  1. Creating/updating/deleting objects from the admin interface
  2. Making changes through the API
  3. Triggering operations from your theme

Common Patterns

Conditional Processing

js
Nimbu.Cloud.before('channel.entries.updated', 'products', (req, res) => {
  // Only process if price changed
  if (req.changes.price) {
    const [oldPrice, newPrice] = req.changes.price;

    if (newPrice < oldPrice * 0.5) {
      res.error('price', 'Price cannot be reduced by more than 50%');
      return;
    }
  }

  res.success();
});

Multi-field Validation

js
Nimbu.Cloud.before('customer.created', (req, res) => {
  const errors = [];

  if (!req.object.get('email')) {
    errors.push(['email', 'Email is required']);
  }

  if (!req.object.get('name')) {
    errors.push(['name', 'Name is required']);
  }

  if (errors.length > 0) {
    errors.forEach(([field, message]) => res.error(field, message));
    return;
  }

  res.success();
});

Derived Data

js
Nimbu.Cloud.before('channel.entries.created', 'products', (req, res) => {
  const price = req.object.get('price');
  const taxRate = 0.21;

  // Auto-calculate price with tax
  req.object.set('price_with_tax', price * (1 + taxRate));

  res.success();
});

Next Steps