Cron jobs have access to the same side-effect APIs as extension hooks: get, mutate,
email, and logger.
get#
Read rows from any table. Returns untyped maps:
final expiredRows = await get.many(
tableName: 'subscriptions',
where: Lt('expires_at', DateTime.now()),
limit: 100,
) ?? [];
See Side Effects: get for full documentation.
mutate#
Insert, update, or delete rows. In cron jobs mutate calls go through the full pipeline (rules, operations, extensions) using the CronJwt system identity:
mutate.delete.many(
tableName: 'old_logs',
updates: [],
where: Lt('created_at', cutoff),
);
mutate.update.one(
table: 'users',
updates: [Update.column('status', .literal('inactive'))],
where: Eq('id', userId),
);
See Side Effects: mutate for full documentation.
email#
Send transactional email using built-in helpers or custom templates:
email.send.loginNotice(
EmailAddress(address: admin['email'] as String),
table: 'admins',
);
email.send(Email(
to: EmailAddress(address: user['email'] as String),
subject: 'Your subscription is expiring',
template: 'subscription_expiry',
variables: {'days': daysLeft},
));
See Side Effects: email for full documentation.
logger#
Write structured log entries visible in server output and the _log table:
logger.info('Processed ${rows.length} rows');
logger.warn('Found ${stale.length} stale subscriptions');
logger.error('Failed to send digest: $error');
Complete Example#
final class ExpiryNotificationJob extends CronJob {
ExpiryNotificationJob()
: super(
name: 'expiry-notifications',
schedule: Schedule.parse('0 9 * * *'),
);
@override
Future<void> run() async {
final cutoff = DateTime.now().add(const Duration(days: 7));
final expiring = await get.many(
tableName: 'subscriptions',
where: And(Lt('expires_at', cutoff), Eq('notified', false)),
) ?? [];
logger.info('Sending expiry notices to ${expiring.length} users');
for (final row in expiring) {
email.send(Email(
to: EmailAddress(address: row['email'] as String),
subject: 'Your subscription expires soon',
template: 'expiry_notice',
variables: {'expires_at': row['expires_at']},
));
mutate.update.one(
table: 'subscriptions',
updates: [Update.column('notified', .literal(true))],
where: Eq('id', row['id']),
);
}
}
}
ExpiryNotificationJob main() => ExpiryNotificationJob();
