Yoke.sh
SDK

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:

  1. Runtime appends the configured event to stream_events
  2. Async fan-out delivers to matching instances
  3. 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/:key

See 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

OpDirectionPurpose
schedule_upsertTS → RustCreate/replace schedule
schedule_cancelTS → RustCancel by instance + key
schedule_listTS → RustList 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_000

Design notes

Use schedules forUse append + actor filter for
Delayed one-shot actionsImmediate reactions
Debounced remindersContinuous 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.

On this page