Background Jobs
Background jobs allow you to run scheduled tasks and long-running operations asynchronously. Use them for automated processes like sending newsletters, data cleanup, or syncing with external systems.
What are Background Jobs?
Background jobs are server-side functions that:
- Run asynchronously without blocking other operations
- Can be scheduled to run at specific times or intervals
- Execute long-running processes that would timeout in callbacks or routes
- Automate repetitive tasks
- Process data in batches
Defining Jobs
Use Nimbu.Cloud.job() to define a background job:
Nimbu.Cloud.job('jobName', async (params) => {
// Your job logic here
console.log('Job started');
// Process data...
console.log('Job completed');
});Job Names
- Use descriptive names:
send_newsletter,cleanup_old_data,sync_inventory - Use snake_case for consistency
- Names must be unique within your Cloud Code
Scheduling Jobs
Use Nimbu.Cloud.schedule() to schedule a job to run:
Nimbu.Cloud.schedule(jobName, params, timing);Parameters
| Parameter | Type | Description |
|---|---|---|
jobName | string | Name of the job to run |
params | object | Data to pass to the job |
timing | object | When to run the job |
Timing Options
// Run immediately
Nimbu.Cloud.schedule('myJob', {}, {});
// Run at specific time
Nimbu.Cloud.schedule('myJob', {}, {
at: new Date('2025-12-25T10:00:00Z')
});
// Run after delay
Nimbu.Cloud.schedule('myJob', {}, {
in: '30m' // Supports: s (seconds), m (minutes), h (hours)
});
// Run on recurring schedule (cron)
Nimbu.Cloud.schedule('myJob', {}, {
every: '0 9 * * *' // Cron expression
});Cron Expressions
For recurring jobs, use cron expressions:
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday = 0)
│ │ │ │ │
* * * * *Common Cron Patterns
// Every hour
{ every: '0 * * * *' }
// Every day at 9 AM
{ every: '0 9 * * *' }
// Every day at 9:15 AM
{ every: '15 9 * * *' }
// Every Monday at 9 AM
{ every: '0 9 * * 1' }
// Every 1st of the month at midnight
{ every: '0 0 1 * *' }
// Every 15 minutes
{ every: '*/15 * * * *' }
// Weekdays at 6 PM
{ every: '0 18 * * 1-5' }Accessing Job Parameters
Access parameters passed to the job:
Nimbu.Cloud.job('process_order', async (params) => {
const orderId = params.orderId;
const action = params.action;
console.log(`Processing order ${orderId} with action ${action}`);
// Process the order...
});
// Schedule the job with parameters
Nimbu.Cloud.schedule('process_order', {
orderId: '12345',
action: 'fulfill'
}, {});Real-World Examples
Example 1: Newsletter Job
Send newsletters on a schedule (real example from theme-hr-gids):
const Mail = require('mail');
// Define the newsletter job
Nimbu.Cloud.job('hr_wijzers_check_new', async () => {
console.log('[CHECK] Checking HR-wijzers for newsletter');
const today = moment().toISOString();
// Find articles to publish today
const query = new Nimbu.Query('hr_wijzers');
query.equalTo('publish_date', today);
const articles = await query.collection().fetch();
if (articles.length === 0) {
console.log('[CHECK] No articles to publish');
return;
}
// Get current month's subscribers
const month = moment().format('MM');
const year = moment().format('YYYY');
const abonneeRole = await getRole(`abonnees_${month}_${year}`);
const subscribers = await abonneeRole.getCustomers().list();
if (subscribers.length === 0) {
console.log('[CHECK] No subscribers found');
return;
}
// Send newsletter to each subscriber
for (const customer of subscribers) {
// Skip if opted out
if (customer.get('newsletter_hrwijzer_off')) {
continue;
}
await Mail.send({
to: customer.get('email'),
template: 'newsletter_hr_wijzers',
variables: {
articles: articles.map(a => a.id),
customerNumber: customer.get('number')
}
});
}
console.log(`[CHECK] Newsletter sent to ${subscribers.length} subscribers`);
});
// Schedule to run daily at 9:15 AM
Nimbu.Cloud.schedule('hr_wijzers_check_new', {}, {
every: '15 9 * * *'
});Example 2: Monthly Role Update
Update customer roles monthly (real example from theme-hr-gids):
Nimbu.Cloud.job('add_abonnees_to_month_role', async () => {
console.log('[CHECK] Checking abonnees for this month');
const today = moment().toISOString();
const thisMonth = moment().format('MM');
const thisYear = moment().format('YYYY');
// Find active subscriptions
const query = new Nimbu.Query('abonnees');
query.lessThanOrEqualTo('start_date', today);
query.greaterThanOrEqualTo('end_date', today);
const activeSubscriptions = await query.collection().fetch();
if (activeSubscriptions.length === 0) {
console.log('[CHECK] No active subscriptions');
return;
}
// Get or create role for this month
const abonneeRole = await getOrCreateRole(`abonnees_${thisMonth}_${thisYear}`);
// Add customers to role
for (const subscription of activeSubscriptions) {
const customer = subscription.get('customer');
abonneeRole.getCustomers().add(customer);
}
await abonneeRole.save();
console.log(`[CHECK] Added ${activeSubscriptions.length} customers to role`);
});
// Schedule to run daily
Nimbu.Cloud.schedule('add_abonnees_to_month_role', {}, {
every: '0 2 * * *' // Daily at 2 AM
});Example 3: Data Cleanup
Clean up old data periodically:
Nimbu.Cloud.job('cleanup_old_sessions', async () => {
console.log('[Cleanup] Starting session cleanup');
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// Find old sessions
const query = new Nimbu.Query('sessions');
query.lessThan('created_at', thirtyDaysAgo);
const oldSessions = await query.collection().fetch();
// Delete each session
for (const session of oldSessions) {
await session.destroy();
}
console.log(`[Cleanup] Deleted ${oldSessions.length} old sessions`);
});
// Run nightly at 2 AM
Nimbu.Cloud.schedule('cleanup_old_sessions', {}, {
every: '0 2 * * *'
});Example 4: Inventory Sync
Sync inventory with external system:
const HTTP = require('http');
const SiteVariables = require('site_variables');
Nimbu.Cloud.job('sync_inventory', async (params) => {
console.log('[Sync] Starting inventory sync');
const apiKey = SiteVariables.get('INVENTORY_API_KEY');
// Fetch all products
const query = new Nimbu.Query('products');
query.equalTo('active', true);
const products = await query.collection().fetch();
let syncedCount = 0;
for (const product of products) {
try {
// Fetch stock from external system
const response = await HTTP.get(
`https://api.inventory.com/stock/${product.get('sku')}`,
{
headers: { 'Authorization': `Bearer ${apiKey}` }
}
);
const data = JSON.parse(response.body);
// Update product stock
product.set('stock_quantity', data.quantity);
await product.save();
syncedCount++;
} catch (error) {
console.error(`Failed to sync product ${product.id}:`, error.message);
}
}
console.log(`[Sync] Synced ${syncedCount} products`);
});
// Run every 6 hours
Nimbu.Cloud.schedule('sync_inventory', {}, {
every: '0 */6 * * *'
});Example 5: Weekly Report
Generate and email weekly reports:
const Mail = require('mail');
Nimbu.Cloud.job('weekly_sales_report', async () => {
console.log('[Report] Generating weekly sales report');
// Get date range
const today = new Date();
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
// Query orders from last week
const query = new Nimbu.Query('orders');
query.greaterThan('created_at', weekAgo);
query.equalTo('status', 'completed');
const orders = await query.collection().fetch();
// Calculate stats
const totalRevenue = orders.reduce((sum, order) => {
return sum + (order.get('total') || 0);
}, 0);
const totalOrders = orders.length;
const avgOrderValue = totalRevenue / totalOrders;
// Send report
await Mail.send({
to: '[email protected]',
subject: 'Weekly Sales Report',
html: `
<h1>Weekly Sales Report</h1>
<p>Period: ${weekAgo.toLocaleDateString()} - ${today.toLocaleDateString()}</p>
<ul>
<li>Total Orders: ${totalOrders}</li>
<li>Total Revenue: €${totalRevenue.toFixed(2)}</li>
<li>Average Order Value: €${avgOrderValue.toFixed(2)}</li>
</ul>
`
});
console.log('[Report] Weekly report sent');
});
// Run every Monday at 9 AM
Nimbu.Cloud.schedule('weekly_sales_report', {}, {
every: '0 9 * * 1'
});Example 6: Abandoned Cart Reminder
Send reminders for abandoned carts:
const Mail = require('mail');
Nimbu.Cloud.job('abandoned_cart_reminder', async () => {
console.log('[Cart] Checking for abandoned carts');
// Find carts abandoned more than 1 hour ago
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
const query = new Nimbu.Query('carts');
query.lessThan('updated_at', oneHourAgo);
query.equalTo('status', 'abandoned');
query.equalTo('reminder_sent', false);
const abandonedCarts = await query.collection().fetch();
for (const cart of abandonedCarts) {
const customer = cart.get('customer');
if (customer && customer.get('email')) {
await Mail.send({
to: customer.get('email'),
template: 'abandoned_cart',
variables: {
cartUrl: cart.get('recovery_url'),
items: cart.get('items')
}
});
cart.set('reminder_sent', true);
await cart.save();
}
}
console.log(`[Cart] Sent ${abandonedCarts.length} reminders`);
});
// Run every hour
Nimbu.Cloud.schedule('abandoned_cart_reminder', {}, {
every: '0 * * * *'
});Example 7: Batch Processing with Parameters
Process orders in batches:
Nimbu.Cloud.job('process_pending_orders', async (params) => {
const batchSize = params.batchSize || 50;
console.log(`[Process] Processing up to ${batchSize} orders`);
const query = new Nimbu.Query('orders');
query.equalTo('status', 'pending');
query.limit(batchSize);
const orders = await query.collection().fetch();
for (const order of orders) {
try {
// Process payment
await processPayment(order);
// Update status
order.set('status', 'processing');
await order.save();
} catch (error) {
console.error(`Failed to process order ${order.id}:`, error.message);
}
}
console.log(`[Process] Processed ${orders.length} orders`);
});
// Schedule with custom batch size
Nimbu.Cloud.schedule('process_pending_orders', {
batchSize: 100
}, {
every: '*/30 * * * *' // Every 30 minutes
});Triggering Jobs Manually
From Routes
Nimbu.Cloud.post('/admin/trigger-sync', (req, res) => {
// Trigger job immediately
Nimbu.Cloud.schedule('sync_inventory', {}, {});
res.json({ status: 'Job scheduled' });
});From Extensions
Nimbu.Cloud.extend('channel.entries.list', 'products',
{ name: 'Trigger Sync' },
(req, res) => {
Nimbu.Cloud.schedule('sync_inventory', {}, {});
res.success('Sync job started');
}
);From Callbacks
Nimbu.Cloud.after('order.created', (req, res) => {
// Queue fulfillment job
Nimbu.Cloud.schedule('fulfill_order', {
orderId: req.object.id
}, {
in: '5m' // Wait 5 minutes before fulfilling
});
res.success();
});Unscheduling Jobs
Remove a scheduled job:
// Unschedule all instances of a job
Nimbu.Cloud.unschedule('jobName', {}, {});
// Unschedule specific instance
Nimbu.Cloud.unschedule('jobName', { orderId: '123' }, {});
// Unschedule job scheduled at specific time
Nimbu.Cloud.unschedule('jobName', {}, {
at: new Date('2025-12-25T10:00:00Z')
});Best Practices
1. Keep Jobs Idempotent
Jobs should produce the same result if run multiple times:
Nimbu.Cloud.job('send_reminder', async (params) => {
const { customerId } = params;
const customer = await new Nimbu.Query('customers').get(customerId);
// Check if reminder already sent
if (customer.get('reminder_sent_at')) {
console.log('Reminder already sent, skipping');
return;
}
await sendReminder(customer);
// Mark as sent
customer.set('reminder_sent_at', new Date());
await customer.save();
});2. Log Progress and Errors
Nimbu.Cloud.job('bulk_update', async (params) => {
console.log('[Bulk Update] Starting at', new Date());
console.log('[Bulk Update] Processing', params.count, 'items');
try {
// Process items...
console.log('[Bulk Update] Completed successfully');
} catch (error) {
console.error('[Bulk Update] Failed:', error.message);
throw error;
}
});3. Handle Failures Gracefully
Nimbu.Cloud.job('sync_data', async () => {
const failedItems = [];
for (const item of items) {
try {
await syncItem(item);
} catch (error) {
console.error(`Failed to sync ${item.id}:`, error.message);
failedItems.push(item.id);
}
}
if (failedItems.length > 0) {
console.log('Failed items:', failedItems);
// Could schedule retry job here
}
});4. Use Parameters for Flexibility
Nimbu.Cloud.job('cleanup_data', async (params) => {
const daysOld = params.daysOld || 30;
const dryRun = params.dryRun || false;
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
const query = new Nimbu.Query('logs');
query.lessThan('created_at', cutoffDate);
const items = await query.collection().fetch();
if (dryRun) {
console.log(`Would delete ${items.length} items (dry run)`);
return;
}
// Actually delete items...
});
// Schedule with different parameters
Nimbu.Cloud.schedule('cleanup_data', { daysOld: 90 }, { every: '0 3 * * *' });5. Avoid Timeouts
// Good: Process in batches
Nimbu.Cloud.job('process_all', async () => {
const batchSize = 100;
let processed = 0;
while (true) {
const query = new Nimbu.Query('items');
query.equalTo('processed', false);
query.limit(batchSize);
const batch = await query.collection().fetch();
if (batch.length === 0) break;
for (const item of batch) {
await processItem(item);
}
processed += batch.length;
}
console.log(`Processed ${processed} items`);
});
// Avoid: Processing too much at once
Nimbu.Cloud.job('process_all_at_once', async () => {
const query = new Nimbu.Query('items');
const allItems = await query.collection().fetch(); // Could be thousands!
for (const item of allItems) {
await processItem(item); // Might timeout!
}
});6. Monitor Job Execution
Nimbu.Cloud.job('important_job', async () => {
const startTime = Date.now();
try {
console.log('[Job] Starting execution');
// Do work...
const duration = Date.now() - startTime;
console.log(`[Job] Completed in ${duration}ms`);
// Could send success notification
} catch (error) {
console.error('[Job] Failed:', error.message);
// Could send alert email
await Mail.send({
to: '[email protected]',
subject: 'Job Failed',
text: `Job failed with error: ${error.message}`
});
throw error;
}
});Common Patterns
Retry Logic
async function withRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Retry ${i + 1}/${maxRetries}`);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
Nimbu.Cloud.job('sync_with_retry', async () => {
await withRetry(async () => {
await syncExternalAPI();
});
});Progressive Processing
Nimbu.Cloud.job('progressive_update', async (params) => {
const lastProcessedId = params.lastId || null;
const batchSize = 100;
const query = new Nimbu.Query('products');
if (lastProcessedId) {
query.greaterThan('id', lastProcessedId);
}
query.limit(batchSize);
query.ascending('id');
const batch = await query.collection().fetch();
for (const product of batch) {
await updateProduct(product);
}
// Schedule next batch if there are more items
if (batch.length === batchSize) {
Nimbu.Cloud.schedule('progressive_update', {
lastId: batch[batch.length - 1].id
}, {});
}
});Viewing Job Logs
Check the Cloud Code logs in the Nimbu admin interface to see:
- When jobs are executed
- Console output from jobs
- Errors that occurred
- Execution duration
Next Steps
- Learn about Callbacks - React to data changes
- Explore Cloud Functions - Callable server-side functions
- Review Routes - Create custom HTTP endpoints
- Check Extensions - Add custom admin actions
- Browse Available Modules - HTTP, Mail, and more