Skip to content

Recipe: Scheduled Cleanup & Maintenance

Run periodic maintenance tasks — expire old records, generate reports, sync external data — using scheduled triggers.


Problem

Your app accumulates data that needs periodic cleanup: expired sessions, stale drafts, completed orders older than 90 days, or external data that needs regular syncing. You need a cron-like mechanism to run these tasks automatically.

Solution

Create a function with a scheduled trigger that runs on an interval or cron schedule.


Example: Delete expired records

Step 1: Create the function

async function run() {
  const collectionName = triggerParams.collection || 'sessions';
  const maxAgeDays = triggerParams.maxAgeDays || 30;

  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);

  // Query records older than the cutoff
  const expired = await api.queryRecords(collectionName, {
    'data.expiresAt[lt]': cutoffDate.toISOString(),
    pageSize: 100,
  });

  if (!expired.data || expired.data.length === 0) {
    api.log({ message: 'No expired records found', collection: collectionName });
    return { success: true, data: { deleted: 0 } };
  }

  let deleted = 0;
  for (const record of expired.data) {
    try {
      await api.deleteRecord(collectionName, record.id);
      deleted++;
    } catch (error) {
      api.log({ error: error.message, recordId: record.id });
    }
  }

  api.log({ message: 'Cleanup complete', collection: collectionName, deleted });
  return { success: true, data: { deleted, checked: expired.data.length } };
}

Step 2: Create a scheduled trigger

  1. In the Console, go to Functions → Triggers → New Trigger
  2. Select Scheduled as the trigger type
  3. Set the schedule:
    • Interval: every 6 hours (21600 seconds)
    • Or cron: 0 2 * * * (daily at 2 AM UTC)
  4. Set static params:
    {
      "collection": "sessions",
      "maxAgeDays": 30
    }
    

That's it — the function runs automatically on the schedule.


Example: Generate a daily summary

Aggregate data and store a summary record for reporting:

async function run() {
  const today = new Date().toISOString().split('T')[0];

  // Count orders by status
  const pending = await api.queryRecords('orders', {
    'data.status': 'pending',
    pageSize: 1,
  });

  const completed = await api.queryRecords('orders', {
    'data.status': 'completed',
    'data.completedAt[gte]': `${today}T00:00:00.000Z`,
    pageSize: 1,
  });

  const summary = {
    date: today,
    pendingOrders: pending.meta?.total || 0,
    completedToday: completed.meta?.total || 0,
    generatedAt: new Date().toISOString(),
  };

  // Store the summary as a record
  await api.createRecord('daily-summaries', summary);

  api.log({ message: 'Daily summary generated', summary });
  return { success: true, data: summary };
}

Example: Sync data from an external API

Pull data from an external source on a schedule:

async function run() {
  const apiKey = triggerParams.externalApiKey;
  const lastSyncField = 'data.lastSyncedAt';

  // Fetch latest data from external API
  const externalData = await api.httpGet(
    'https://api.example.com/v1/products',
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  );

  if (!externalData || !externalData.products) {
    return { success: false, error: 'Failed to fetch external data' };
  }

  let synced = 0;
  for (const product of externalData.products) {
    // Upsert: check if record exists, update or create
    const existing = await api.queryRecords('products', {
      'data.externalId': product.id,
      pageSize: 1,
    });

    if (existing.data && existing.data.length > 0) {
      await api.updateRecord('products', existing.data[0].id, {
        name: product.name,
        price: product.price,
        lastSyncedAt: new Date().toISOString(),
      });
    } else {
      await api.createRecord('products', {
        externalId: product.id,
        name: product.name,
        price: product.price,
        lastSyncedAt: new Date().toISOString(),
      });
    }
    synced++;
  }

  api.log({ message: 'Sync complete', synced });
  return { success: true, data: { synced } };
}

Add the external API domain to Logic → Domains.


Common gotchas

  • Execution timeout: Scheduled functions have the same timeout as other functions. If you're processing thousands of records, paginate and process in batches. Consider breaking large jobs into multiple function runs.
  • Overlapping runs: If a scheduled function takes longer than the interval, the next run will start while the previous one is still running. Design functions to be idempotent (safe to run twice on the same data).
  • Empty executionParams: Scheduled triggers pass {} as executionParams. All config goes in triggerParams (the static params on the trigger).
  • Timezone: Cron schedules run in UTC. If you need local time logic, convert inside the function.
  • Monitoring: Check Functions → Runs in the Console or the Trigger Health page to verify your scheduled function is running and succeeding.