Schedules
Keyed delayed events — ctx.schedule, ctx.schedules, schedule_upsert, and assistant.follow_up_due pattern.
Schedules let actor instances fire delayed stream events. Each schedule is keyed — upserting the same key replaces the previous fire time. When due, the runtime appends the configured event to the global stream (same durability as ctx.append).
API
Schedules are available on ActorCtx and PortalCtx:
// Relative delay
await ctx.schedules.schedule({
key: "reminder:order-42",
delay: { minutes: 30 },
event: {
type: "order.reminder",
payload: { orderId: "42" },
},
});
// Absolute time
await ctx.schedules.schedule({
key: "reminder:order-42",
at: Date.now() + 3_600_000,
event: { type: "order.reminder", payload: { orderId: "42" } },
});
// Cancel
await ctx.schedules.cancel("reminder:order-42");
// List
const pending = await ctx.schedules.list({ keyPrefix: "reminder:" });ScheduleInput
type ScheduleInput = {
key: string; // stable id for replace/cancel
event: AppendInput; // typed event to append when due
at?: number; // absolute fire time (epoch ms)
delay?: Duration; // relative — mutually exclusive with at
correlation?: AppendOptions["correlation"];
};
type Duration =
| number
| { ms?: number; seconds?: number; minutes?: number; hours?: number };Ownership
Schedules are owned by the creating instance. The kernel stores instance_id with each schedule row. When an instance is destroyed, its schedules are cancelled.
Use descriptive key prefixes to avoid collisions within an instance:
function scheduleKey(forumId: string) {
return `follow-up:${forumId}`;
}Fire semantics
When fire_at elapses:
- Runtime appends the configured event to
stream_events - Async fan-out delivers to matching instances
- Schedule row is deleted (one-shot) or rescheduled (if upserted again)
Correlation from the schedule input is applied to the appended event.
HTTP API (control plane)
Headless apps can manage schedules without an actor context:
POST /v1/tenants/:tenant_id/schedules
GET /v1/tenants/:tenant_id/schedules
DELETE /v1/tenants/:tenant_id/schedules/:keySee HTTP API reference.
Follow-up actor pattern
The SDK defineFollowUpActor uses schedules for agent nudges:
// On agent message in forum → schedule follow-up
await ctx.schedules.schedule({
key: scheduleKey(forumId),
delay: { ms: ctx.config.delayMs },
event: {
type: "assistant.follow_up_due",
payload: {
forumId,
forumParticipantId: ctx.config.forumParticipantId,
},
},
});
// On human message → cancel
await ctx.schedules.cancel(scheduleKey(forumId));When assistant.follow_up_due fires, the follow-up actor posts a forum message with meta.followUp: true so it does not re-arm itself.
Ops bridge
| Op | Direction | Purpose |
|---|---|---|
schedule_upsert | TS → Rust | Create/replace schedule |
schedule_cancel | TS → Rust | Cancel by instance + key |
schedule_list | TS → Rust | List schedules for instance |
Helpers
import { resolveDuration, resolveFireAt } from "@yoke/sdk";
resolveDuration({ minutes: 5, seconds: 30 }); // 330_000 ms
resolveFireAt({ delay: { hours: 1 } }); // Date.now() + 3_600_000Design notes
| Use schedules for | Use append + actor filter for |
|---|---|
| Delayed one-shot actions | Immediate reactions |
| Debounced reminders | Continuous polling |
| Replacing pending timers (same key) | Recurring cron (use portal start() loop) |
Schedules persist in Postgres — they survive runtime restart and fire when due even if the instance was idle.