Skip to main content

Schedulers

Execute functions at defined time intervals.

Why Use Schedulers

Your application needs recurring background tasks: cleaning up expired records, sending daily email digests, syncing data from external APIs, or rotating credentials. Without schedulers, you'd manage cron infrastructure, ensure fault tolerance, and handle deployment coordination yourself.

With schedulers, you decorate a function and deploy:

Backend code
// A decorated method that runs on a schedule
@scheduler('cleanupExpiredSessions', CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanupExpiredSessions(): Promise<void> {
const sessions = this.squid.collection('sessions');
const expired = await sessions.query().where('expiresAt', '<', new Date()).dereference().snapshot();
for (const session of expired) {
await sessions.doc(session.id).delete();
}
}

No cron servers. No infrastructure. Just functions that run on time.

Overview

Schedulers are backend functions that run automatically at defined intervals based on UTC time. They are ideal for recurring background work that does not require a client request to trigger.

When to use schedulers

Use CaseRecommendation
Run recurring background tasks on a schedule✅ Scheduler
React to database changesUse Triggers
Call a function from the clientUse Executables
Expose an HTTP endpoint to external servicesUse Webhooks

How it works

  1. You decorate a method with @scheduler() in a class that extends SquidService
  2. You specify a cron expression or CronExpression enum value for the schedule
  3. Squid discovers and registers the scheduler at deploy time
  4. The function runs automatically at each scheduled interval in UTC
  5. Logs and errors are available through the Squid Console

Quick Start

Prerequisites

  • A Squid backend project initialized with squid init-backend
  • The @squidcloud/backend package installed

Step 1: Create a scheduler

Create a service class that extends SquidService and add your scheduler function:

Backend code
import { CronExpression, SquidService, scheduler } from '@squidcloud/backend';

export class ExampleService extends SquidService {
@scheduler('logHeartbeat', CronExpression.EVERY_MINUTE)
async logHeartbeat(): Promise<void> {
console.log('Scheduler is running:', new Date().toISOString());
}
}

Step 2: Export the service

Ensure your service is exported from the service index file:

Backend code
export * from './example-service';

Step 3: Start or deploy the backend

For local development, run the backend locally using the Squid CLI:

squid start

To deploy to the cloud, see deploying your backend.

Step 4: Verify

Check the Squid Console logs to confirm the scheduler is running at the expected interval.

Core Concepts

Cron expressions

The @scheduler decorator accepts a cron expression string that defines when the function runs. All times are in Coordinated Universal Time (UTC). Convert your desired local times to UTC when defining the expression.

A cron expression follows this format:

* * * * * *
| | | | | |
| | | | | day of week
| | | | months
| | | day of month
| | hours
| minutes
seconds (optional)

Examples:

ExpressionSchedule
0 0 * * *Every day at midnight UTC
0 */6 * * *Every 6 hours
30 9 * * 1-5Weekdays at 9:30 AM UTC
0 0 1 * *First day of every month at midnight UTC

CronExpression enum

The CronExpression enum provides predefined intervals so you don't need to write cron strings manually:

Backend code
import { CronExpression, scheduler } from '@squidcloud/backend';

@scheduler('everyMinute', CronExpression.EVERY_MINUTE)
async everyMinute(): Promise<void> { /* ... */ }

@scheduler('everyHour', CronExpression.EVERY_HOUR)
async everyHour(): Promise<void> { /* ... */ }

@scheduler('daily', CronExpression.EVERY_DAY_AT_MIDNIGHT)
async daily(): Promise<void> { /* ... */ }

The exclusive parameter

The @scheduler decorator accepts three parameters: the scheduler name, the cron expression, and an optional exclusive boolean.

When exclusive is true (the default), only one instance of the scheduler runs at a time. If a new invocation is scheduled while the previous one is still running, the new invocation is skipped.

Backend code
// Default: exclusive is true, so overlapping runs are skipped
@scheduler('sendEmailReminders', CronExpression.EVERY_MINUTE, true)
async sendEmailReminders(): Promise<void> {
// If this takes longer than 1 minute, the next invocation is skipped
}

When exclusive is false, a new instance runs on schedule regardless of whether the previous instance has completed. Multiple instances may run concurrently.

Backend code
// Non-exclusive: allows concurrent runs
@scheduler('processQueue', CronExpression.EVERY_MINUTE, false)
async processQueue(): Promise<void> {
// Multiple instances may run in parallel
}

Managing schedulers

Schedulers can be disabled, re-enabled, and listed programmatically.

Disable and enable:

A disabled scheduler does not run until it is re-enabled. The disabled state persists across redeploys, but all schedulers are re-enabled on a deploy that follows an undeploy.

Backend code
// Disable a scheduler
await this.squid.schedulers.disable('logHeartbeat');

// Re-enable it later
await this.squid.schedulers.enable('logHeartbeat');

List all schedulers:

Returns all registered schedulers with their current state (enabled or disabled).

Backend code
const allSchedulers = await this.squid.schedulers.list();
console.log(allSchedulers);

Error Handling

What happens when a scheduler throws

If a scheduler function throws an error, the error is logged and the scheduler continues to run at its next scheduled interval. A single failure does not disable the scheduler.

Logging and debugging

Use console.log and console.error within scheduler functions. Output is available in the Squid Console logs.

Backend code
@scheduler('syncExternalData', CronExpression.EVERY_HOUR)
async syncExternalData(): Promise<void> {
console.log('Starting external data sync');
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`API returned status ${response.status}`);
}
const data = (await response.json()) as any[];
console.log(`Synced ${data.length} records`);
} catch (error) {
console.error('Sync failed:', error);
}
}

Common errors

ErrorCauseSolution
Scheduler not runningService not exported in service/index.tsEnsure the service class is exported
Scheduler not runningBackend not deployed after changesRun squid deploy
Overlapping runs skippedexclusive is true and previous run is still activeIncrease the interval or set exclusive to false
Incorrect timingCron expression uses local time instead of UTCConvert all times to UTC

Best Practices

  1. Design for idempotency. Schedulers may occasionally run more than expected due to retries or redeployments. Ensure your logic produces the same result if executed multiple times.

  2. Keep execution time short. Long-running schedulers can overlap with the next invocation or consume excessive resources. Break large tasks into smaller batches.

  3. Use exclusive wisely. Leave exclusive as true (the default) for tasks that should not overlap, like database cleanup. Set it to false only when concurrent execution is safe, such as processing independent queue items.

  4. Handle errors inside the scheduler. Wrap your logic in try/catch blocks and log errors. This prevents a transient failure from producing an unhandled exception.

  5. Monitor your schedulers. Use the Squid Console to verify that schedulers are running on time. Use this.squid.schedulers.list() to programmatically check scheduler states.

  6. Protect shared resources. If your scheduler modifies shared data, use rate and quota limiting on dependent services to avoid overwhelming downstream systems.

Code Examples

Database cleanup

Remove expired records on a daily schedule:

Backend code
import { CronExpression, SquidService, scheduler } from '@squidcloud/backend';

interface Session {
id: string;
userId: string;
expiresAt: Date;
}

export class CleanupService extends SquidService {
@scheduler('cleanupExpiredSessions', CronExpression.EVERY_DAY_AT_MIDNIGHT)
async cleanupExpiredSessions(): Promise<void> {
const sessions = this.squid.collection<Session>('sessions');
const expired = await sessions.query().where('expiresAt', '<', new Date()).dereference().snapshot();

for (const session of expired) {
await sessions.doc(session.id).delete();
}

console.log(`Cleaned up ${expired.length} expired sessions`);
}
}

Sending email digests

Send a weekly summary email every Monday at 9 AM UTC:

Backend code
import { SquidService, scheduler } from '@squidcloud/backend';

interface UserActivity {
userId: string;
email: string;
actionsThisWeek: number;
}

export class NotificationService extends SquidService {
@scheduler('sendWeeklyDigest', '0 9 * * 1') // Monday at 9:00 AM UTC
async sendWeeklyDigest(): Promise<void> {
const users = this.squid.collection<UserActivity>('userActivity');
const activeUsers = await users.query().where('actionsThisWeek', '>', 0).dereference().snapshot();

for (const user of activeUsers) {
await this.sendDigestEmail(user.email, user.actionsThisWeek);
}

console.log(`Sent digest to ${activeUsers.length} users`);
}

private async sendDigestEmail(email: string, actionCount: number): Promise<void> {
const apiKey = this.secrets['EMAIL_API_KEY'];
await fetch('https://api.email.example.com/send', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: email,
subject: 'Your Weekly Activity Summary',
body: `You completed ${actionCount} actions this week.`,
}),
});
}
}

Syncing external data

Pull data from an external API every hour:

Backend code
import { CronExpression, SquidService, scheduler } from '@squidcloud/backend';

interface Product {
id: string;
name: string;
price: number;
}

export class SyncService extends SquidService {
@scheduler('syncProducts', CronExpression.EVERY_HOUR)
async syncProducts(): Promise<void> {
try {
const apiKey = this.secrets['CATALOG_API_KEY'];
const response = await fetch('https://api.catalog.example.com/products', {
headers: { Authorization: `Bearer ${apiKey}` },
});

if (!response.ok) {
console.error(`Catalog API error: ${response.status}`);
return;
}

const products = (await response.json()) as Product[];
const collection = this.squid.collection<Product>('products');

for (const product of products) {
await collection.doc(product.id).insert(product);
}

console.log(`Synced ${products.length} products`);
} catch (error) {
console.error('Product sync failed:', error);
}
}
}

See Also