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:
// 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 Case | Recommendation |
|---|---|
| Run recurring background tasks on a schedule | ✅ Scheduler |
| React to database changes | Use Triggers |
| Call a function from the client | Use Executables |
| Expose an HTTP endpoint to external services | Use Webhooks |
How it works
- You decorate a method with
@scheduler()in a class that extendsSquidService - You specify a cron expression or
CronExpressionenum value for the schedule - Squid discovers and registers the scheduler at deploy time
- The function runs automatically at each scheduled interval in UTC
- Logs and errors are available through the Squid Console
Quick Start
Prerequisites
- A Squid backend project initialized with
squid init-backend - The
@squidcloud/backendpackage installed
Step 1: Create a scheduler
Create a service class that extends SquidService and add your scheduler function:
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:
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:
| Expression | Schedule |
|---|---|
0 0 * * * | Every day at midnight UTC |
0 */6 * * * | Every 6 hours |
30 9 * * 1-5 | Weekdays 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:
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.
// 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.
// 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.
// 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).
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.
@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
| Error | Cause | Solution |
|---|---|---|
| Scheduler not running | Service not exported in service/index.ts | Ensure the service class is exported |
| Scheduler not running | Backend not deployed after changes | Run squid deploy |
| Overlapping runs skipped | exclusive is true and previous run is still active | Increase the interval or set exclusive to false |
| Incorrect timing | Cron expression uses local time instead of UTC | Convert all times to UTC |
Best Practices
-
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.
-
Keep execution time short. Long-running schedulers can overlap with the next invocation or consume excessive resources. Break large tasks into smaller batches.
-
Use
exclusivewisely. Leaveexclusiveastrue(the default) for tasks that should not overlap, like database cleanup. Set it tofalseonly when concurrent execution is safe, such as processing independent queue items. -
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.
-
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. -
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:
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:
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:
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
- Triggers - React to database changes
- Executables - Expose backend functions to the client
- Webhooks - Expose HTTP endpoints to external services
- Rate and quota limiting - Protect your backend functions
- Database - Access and manage data