Nimbu Developer Docs

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

ParameterTypeDescription
jobNamestringName of the job to run
paramsobjectData to pass to the job
timingobjectWhen 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:=2026

Use 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

On this page