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
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
Nimbu.Cloud.after('created', 'orders', (req, res) => {
// Trigger side effects
// Operation has already completed
});Supported Events
Channel Entries
// 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
// 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
// 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
// Before/after events
Nimbu.Cloud.before('coupon.created', handler);
Nimbu.Cloud.after('coupon.created', handler);
// ... updated, deletedDevices
// Before/after events
Nimbu.Cloud.before('device.created', handler);
Nimbu.Cloud.after('device.created', handler);
// ... updated, deletedOrders (After only)
Orders only support after callbacks:
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:
// 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:
| Property | Description | Example |
|---|---|---|
req.object | The Nimbu.Object being operated on | req.object.get('email') |
req.actor | Customer or user who triggered the action | req.actor (can be null) |
req.user | Backend user (if triggered from admin) | req.user |
req.customer | Alias for req.actor when it's a customer | req.customer |
req.changes | Object with field changes (update/delete only) | req.changes.email |
req.lastUpdatedAt | Previous update timestamp (update only) | Date object |
Accessing Object Data
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:
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:
res.success();Error (Validation)
Block the operation with a validation error:
// 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:
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):
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:
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:
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:
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:
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:
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:
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:
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:
// 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
// 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()
// 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
// 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:
// 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
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
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:
- Creating/updating/deleting objects from the admin interface
- Making changes through the API
- Triggering operations from your theme
Common Patterns
Conditional Processing
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
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
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
- Learn about Cloud Functions - Create callable server-side functions
- Explore Routes - Build custom HTTP endpoints
- Review Background Jobs - Schedule automated tasks
- Check Extensions - Add custom admin actions