Skip to content

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:

js
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:

js
Nimbu.Cloud.schedule(jobName, params, timing);

Parameters

ParameterTypeDescription
jobNamestringName of the job to run
paramsobjectData to pass to the job
timingobjectWhen to run the job

Timing Options

js
// 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

js
// 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:

js
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):

js
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):

js
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:

js
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:

js
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:

js
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:

js
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:

js
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

js
Nimbu.Cloud.post('/admin/trigger-sync', (req, res) => {
  // Trigger job immediately
  Nimbu.Cloud.schedule('sync_inventory', {}, {});

  res.json({ status: 'Job scheduled' });
});

From Extensions

js
Nimbu.Cloud.extend('channel.entries.list', 'products',
  { name: 'Trigger Sync' },
  (req, res) => {
    Nimbu.Cloud.schedule('sync_inventory', {}, {});
    res.success('Sync job started');
  }
);

From Callbacks

js
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:

js
// 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:

js
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

js
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

js
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

js
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

js
// 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

js
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

js
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

js
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