Schedule Tables
Tables can trigger reducers or procedures at specific times by including a special scheduling column. This allows you to schedule future actions like sending reminders, expiring items, or running periodic maintenance tasks.
Defining a Schedule Table
The table attribute uses scheduled (with a "d") because it refers to the scheduled reducer - the function that will be scheduled for execution. The table itself is a "schedule table" that stores schedules, while the reducer it triggers is a "scheduled reducer".
- TypeScript
- C#
- Rust
const reminder = table(
{ name: 'reminder', scheduled: 'send_reminder' },
{
scheduled_id: t.u64().primaryKey().autoInc(),
scheduled_at: t.scheduleAt(),
message: t.string(),
}
);
spacetimedb.reducer('send_reminder', { arg: reminder.rowType }, (_ctx, { arg }) => {
// Invoked automatically by the scheduler
// arg.message, arg.scheduled_at, arg.scheduled_id
});[SpacetimeDB.Table(Scheduled = "SendReminder", ScheduledAt = "ScheduleAt")]
public partial struct Reminder
{
[SpacetimeDB.PrimaryKey]
[SpacetimeDB.AutoInc]
public ulong Id;
public uint UserId;
public string Message;
public ScheduleAt ScheduleAt;
}
[SpacetimeDB.Reducer()]
public static void SendReminder(ReducerContext ctx, Reminder reminder)
{
// Process the scheduled reminder
}#[spacetimedb::table(name = reminder_schedule, scheduled(send_reminder))]
pub struct Reminder {
#[primary_key]
#[auto_inc]
id: u64,
user_id: u32,
message: String,
scheduled_at: ScheduleAt,
}
#[spacetimedb::reducer]
fn send_reminder(ctx: &ReducerContext, reminder: Reminder) -> Result<(), String> {
// Process the scheduled reminder
Ok(())
}Inserting Schedules
To schedule an action, insert a row into the schedule table with a scheduled_at value. You can schedule actions to run:
- At intervals - Execute repeatedly at fixed time intervals (e.g., every 5 seconds)
- At specific times - Execute once at an absolute timestamp
Scheduling at Intervals
Use intervals for periodic tasks like game ticks, heartbeats, or recurring maintenance:
- TypeScript
- C#
- Rust
import { ScheduleAt } from 'spacetimedb';
// Schedule to run every 5 seconds (5,000,000 microseconds)
ctx.db.reminder.insert({
scheduled_id: 0n,
scheduled_at: ScheduleAt.interval(5_000_000n),
message: "Check for updates",
});
// Schedule to run every 100 milliseconds
ctx.db.reminder.insert({
scheduled_id: 0n,
scheduled_at: ScheduleAt.interval(100_000n), // 100ms in microseconds
message: "Game tick",
});// Schedule to run every 5 seconds
ctx.Db.Reminder.Insert(new Reminder
{
Message = "Check for updates",
ScheduleAt = new ScheduleAt.Interval(TimeSpan.FromSeconds(5))
});
// Schedule to run every 100 milliseconds
ctx.Db.Reminder.Insert(new Reminder
{
Message = "Game tick",
ScheduleAt = new ScheduleAt.Interval(TimeSpan.FromMilliseconds(100))
});use spacetimedb::{ScheduleAt, Duration};
// Schedule to run every 5 seconds
ctx.db.reminder().insert(Reminder {
id: 0,
message: "Check for updates".to_string(),
scheduled_at: ScheduleAt::Interval(Duration::from_secs(5).into()),
});
// Schedule to run every 100 milliseconds
ctx.db.reminder().insert(Reminder {
id: 0,
message: "Game tick".to_string(),
scheduled_at: ScheduleAt::Interval(Duration::from_millis(100).into()),
});Scheduling at Specific Times
Use specific times for one-shot actions like sending a reminder at a particular moment or expiring content:
- TypeScript
- C#
- Rust
import { ScheduleAt } from 'spacetimedb';
// Schedule for 10 seconds from now
const tenSecondsFromNow = ctx.timestamp.microseconds + 10_000_000n;
ctx.db.reminder.insert({
scheduled_id: 0n,
scheduled_at: ScheduleAt.time(tenSecondsFromNow),
message: "Your auction has ended",
});
// Schedule for a specific Unix timestamp (microseconds since epoch)
const targetTime = 1735689600_000_000n; // Jan 1, 2025 00:00:00 UTC
ctx.db.reminder.insert({
scheduled_id: 0n,
scheduled_at: ScheduleAt.time(targetTime),
message: "Happy New Year!",
});// Schedule for 10 seconds from now
ctx.Db.Reminder.Insert(new Reminder
{
Message = "Your auction has ended",
ScheduleAt = new ScheduleAt.Time(DateTimeOffset.UtcNow.AddSeconds(10))
});
// Schedule for a specific time
var targetTime = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
ctx.Db.Reminder.Insert(new Reminder
{
Message = "Happy New Year!",
ScheduleAt = new ScheduleAt.Time(targetTime)
});use spacetimedb::{ScheduleAt, Duration};
// Schedule for 10 seconds from now
let ten_seconds_from_now = ctx.timestamp + Duration::from_secs(10);
ctx.db.reminder().insert(Reminder {
id: 0,
message: "Your auction has ended".to_string(),
scheduled_at: ScheduleAt::Time(ten_seconds_from_now),
});
// Schedule for immediate execution (current timestamp)
ctx.db.reminder().insert(Reminder {
id: 0,
message: "Process now".to_string(),
scheduled_at: ScheduleAt::Time(ctx.timestamp.clone()),
});How It Works
- Insert a row with a
ScheduleAtvalue - SpacetimeDB monitors the schedule table
- When the time arrives, the specified reducer/procedure is automatically called with the row as a parameter
- The row is typically deleted or updated by the reducer after processing
Security Considerations
Scheduled reducers are normal reducers that can also be invoked by external clients. If a scheduled reducer should only execute via the scheduler, add authentication checks.
- TypeScript
- C#
- Rust
spacetimedb.reducer('send_reminder', { arg: Reminder.rowType }, (ctx, { arg }) => {
if (!ctx.senderAuth.isInternal) {
throw new SenderError('This reducer can only be called by the scheduler');
}
// Process the scheduled reminder
});[SpacetimeDB.Reducer()]
public static void SendReminder(ReducerContext ctx, Reminder reminder)
{
if (!ctx.SenderAuth.IsInternal)
{
throw new Exception("This reducer can only be called by the scheduler");
}
// Process the scheduled reminder
}#[spacetimedb::reducer]
fn send_reminder(ctx: &ReducerContext, reminder: Reminder) -> Result<(), String> {
if !ctx.sender_auth().is_internal() {
return Err("This reducer can only be called by the scheduler".to_string());
}
// Process the scheduled reminder
Ok(())
}Use Cases
- Reminders and notifications - Schedule messages to be sent at specific times
- Expiring content - Automatically remove or archive old data
- Delayed actions - Queue up actions to execute after a delay
- Periodic tasks - Schedule repeating maintenance or cleanup operations
- Game mechanics - Timer-based gameplay events (building completion, energy regeneration, etc.)
Next Steps
- Learn about Reducers to handle scheduled actions
- Explore Procedures for scheduled execution patterns