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…
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. The handler receives a request object; parameters passed when scheduling are available as request.params:
Nimbu.Cloud.job('jobName', async (request) => {
// 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
});From the CLI
Trigger a job remotely with the Nimbu CLI. The job name is passed with the --job flag, and parameters as trailing assignments:
nimbu jobs run --job send_newsletter --site my-site segment=active year:=2026Use key=value for strings and key:=value for typed JSON. Add --wait to tail the job's cloud-code logs after scheduling, and use nimbu jobs list to see which jobs the server has registered.
Job names are unique per site. With --wait, the CLI resolves the owning app from the site's registered jobs, so the command works from any directory without nimbu.yml or an app flag.
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
Parameters passed to the job arrive on the request object, under request.params (the first handler argument is not the params object itself):
Nimbu.Cloud.job('process_order', async (request) => {
const orderId = request.params.orderId;
const action = request.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.models) {
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.models) {
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 () => {
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.models) {
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: 'admin@example.com',
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.models) {
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 (request) => {
const batchSize = request.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.models) {
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 (request) => {
const { customerId } = request.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 (request) => {
console.log('[Bulk Update] Starting at', new Date());
console.log('[Bulk Update] Processing', request.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 (request) => {
const daysOld = request.params.daysOld || 30;
const dryRun = request.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.models) {
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.models) {
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: 'admin@example.com',
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 (request) => {
const lastProcessedId = request.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.models) {
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