Scheduled Reminders
Register recurring schedules that trigger proactive agent messages at specific times, with per-schedule timezones, quiet-hours filtering, and structured inventory linkage. Suitable for medication reminders, habit nudges, exercise check-ins, and any other recurring proactive event.
翻訳待ち. This page is currently in English pending Japanese translation. See CONTRIBUTING for translation workflow.
What you'll build
- A daily 09:00 Asia/Singapore check-in schedule that fires a proactive agent message every morning
- An every-4-hours schedule with a quiet-hours active window that skips fires outside allowed hours
- A bounded
interval_hourscourse constrained bystarts_atandends_at— useful for multi-week programs - An understanding of how the same primitive powers the full Medication Reminders worked example
Scheduled Reminders are a first-class primitive: the platform recomputes next_fire_at after every fire, respects DST transitions automatically, and injects inventory context live at fire time so your agent always has current data.
1. Create a schedule
Register a schedule by calling POST /api/v1/agents/{agentId}/users/{userId}/schedules. The body describes when to fire (cadence), what the agent should do (intent), and optional scoping fields (active_window, inventory_item_id, starts_at, ends_at).
Here is a minimal daily 09:00 SGT check-in:
{
"cadence": {
"simple": { "frequency": "daily", "times": ["09:00"] },
"timezone": "Asia/Singapore"
},
"intent": "check in on how the user is feeling",
"check_type": "reminder"
}And a full example with all optional fields:
{
"cadence": {
"simple": { "frequency": "daily", "times": ["09:00"] },
"timezone": "Asia/Singapore"
},
"active_window": {
"hours": { "start": "08:00", "end": "22:00" },
"days_of_week": ["mon", "tue", "wed", "thu", "fri"]
},
"intent": "check in on how the user is feeling",
"check_type": "reminder",
"inventory_item_id": "01HX8F...",
"metadata": { "campaign": "daily_checkin_v2" },
"starts_at": "2026-05-01T00:00:00Z",
"ends_at": "2026-05-14T23:59:59Z"
}The response includes schedule_id, next_fire_at (UTC), and next_fire_at_local (the same instant expressed in the schedule's timezone — useful for displaying to the user).
import { Sonzai } from "@sonzai-labs/agents";
const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });
const AGENT_ID = "agent_abc";
const USER_ID = "user_123";
const schedule = await client.schedules.create(AGENT_ID, USER_ID, {
cadence: {
simple: { frequency: "daily", times: ["09:00"] },
timezone: "Asia/Singapore",
},
intent: "check in on how the user is feeling",
check_type: "reminder",
});
console.log(schedule.schedule_id); // "sched_01HX..."
console.log(schedule.next_fire_at); // "2026-05-02T01:00:00Z"
console.log(schedule.next_fire_at_local); // "2026-05-02T09:00:00+08:00"2. Cadence shapes
Two mutually exclusive shapes are supported: simple and cron. Exactly one must be present in the cadence object.
Simple cadence
| Field | Type | Required | Description |
|---|---|---|---|
frequency | "daily" | "weekly" | "interval_hours" | Yes | Recurrence pattern |
times | string[] | Yes for daily/weekly | Wall-clock times in HH:MM (24-hour), evaluated in the schedule's timezone |
days_of_week | string[] | Yes for weekly | "mon", "tue", "wed", "thu", "fri", "sat", "sun" |
interval_hours | number | Yes for interval_hours | Minimum 1, maximum 24 |
timezone | IANA string | Yes | Applied to times and days_of_week evaluation |
A weekly schedule fires on the specified days at each listed time. A daily schedule fires every day at each listed time. An interval_hours schedule fires repeatedly at that interval starting from starts_at (or schedule creation if starts_at is omitted), bounded by the active window.
Cron cadence
| Field | Type | Required | Description |
|---|---|---|---|
expression | string | Yes | Standard 5-field cron (min hour dom month dow) |
timezone | IANA string | Yes | Cron fields are evaluated in this zone |
Standard 5-field cron — no seconds field. Example: "0 9 * * 1-5" fires at 09:00 on weekdays.
Rate limits. Cadences that resolve to more than one fire per minute are rejected with CADENCE_TOO_FREQUENT. Cadences that produce more than 96 raw ticks per 24-hour rolling window (before active-window filtering) are rejected with CADENCE_TOO_DENSE. For most use cases interval_hours: 1 (24 raw ticks/day) is the densest practical setting.
3. Timezones
Every schedule requires a timezone field containing a valid IANA timezone name (e.g. "Asia/Singapore", "America/New_York", "Europe/London"). Offsets like "+08:00" are not accepted.
All cadence math — wall-clock time evaluation, days_of_week membership, DST skip logic — runs in the schedule's own timezone. The result is stored and returned as next_fire_at in UTC. next_fire_at_local is a convenience field that expresses the same instant with the zone offset applied.
When a user travels or changes their preferred timezone, patch the schedule timezone directly:
// User moved from Singapore to London
await client.schedules.update(AGENT_ID, USER_ID, scheduleId, {
cadence: {
simple: { frequency: "daily", times: ["09:00"] },
timezone: "Europe/London",
},
});DST handling. On spring-forward transitions, a wall time that falls into the clocks-forward gap (e.g. 02:30 in a zone that jumps 02:00 → 03:00) is non-existent. The platform skips that occurrence and fires at the next valid occurrence. On fall-back transitions, a wall time that exists twice is never double-fired — the platform fires once and advances.
4. Active window (quiet hours + allowed days)
The active_window field restricts which fires actually produce a proactive wakeup. Fires computed by the cadence that land outside the window are skipped, not deferred — the cadence grid stays perfectly predictable and no backlog accumulates.
{
"active_window": {
"hours": { "start": "08:00", "end": "22:00" },
"days_of_week": ["mon", "tue", "wed", "thu", "fri"]
}
}Both sub-fields are optional within active_window. You may specify hours only, days_of_week only, or both.
Overnight windows. When start is greater than end, the window wraps midnight. For example {"start": "22:00", "end": "06:00"} allows fires from 22:00 to 05:59 the next morning. This is useful for night-shift workers or schedules targeting early-morning time zones where local midnight matters.
Allowed days. Values must be lowercase three-letter abbreviations: "mon", "tue", "wed", "thu", "fri", "sat", "sun". Day membership is evaluated in the schedule's timezone, so a fire at 23:30 Friday Singapore time stays Friday even when stored as 15:30 UTC (Saturday in some zones).
Empty days array. Passing "days_of_week": [] (an explicit empty list) is rejected with INVALID_ACTIVE_WINDOW — it would produce a schedule that can never fire. To allow all days, omit the days_of_week field entirely.
5. Linking an inventory item
Pass inventory_item_id on the create (or patch) body to associate a schedule with a specific item from the user's resource inventory. The item's properties are injected live at fire time — not at schedule creation — so any mid-program updates to the item (e.g. a medication dosage change, a price update) are automatically reflected in the agent's proactive message without requiring any schedule modification.
{
"cadence": {
"simple": { "frequency": "daily", "times": ["08:00"] },
"timezone": "Asia/Singapore"
},
"intent": "remind the user to take their morning medication",
"check_type": "reminder",
"inventory_item_id": "01HX8FKZQ3..."
}At fire time the platform fetches the current item properties and appends them to the intent block the agent receives. The Medication Reminders tutorial shows a complete worked example including how to structure medication inventory items for maximum agent context.
Graceful degradation. If the referenced inventory item is deleted before a fire occurs, the schedule continues firing. The intent block is delivered without the Reference item section — the agent receives the intent and metadata fields as normal. No error is surfaced to the user; the schedule itself is not affected.
6. Bounded courses (starts_at / ends_at)
Use starts_at and ends_at to create a time-bounded program. Both fields are optional and accept RFC 3339 UTC timestamps.
{
"cadence": {
"simple": {
"frequency": "interval_hours",
"interval_hours": 4
},
"timezone": "Asia/Singapore"
},
"active_window": {
"hours": { "start": "08:00", "end": "22:00" }
},
"intent": "prompt the user to log a pain score",
"check_type": "check_in",
"starts_at": "2026-05-01T00:00:00Z",
"ends_at": "2026-05-14T23:59:59Z"
}starts_at— no fire is produced before this timestamp. Cadence expansion begins from this point. If omitted, the schedule starts immediately.ends_at— once this timestamp passes, the schedule is automatically disabled (enabledflips tofalse). The row is not deleted, so the audit trail and historical fire log remain accessible.
Passing ends_at that is less than or equal to starts_at returns INVALID_WINDOW. Passing a past ends_at at creation time also returns INVALID_WINDOW — a schedule that has already expired cannot be created.
7. Pausing, editing, deleting
| Operation | How | Behavior |
|---|---|---|
| Pause | PATCH enabled: false | Schedule stops producing fires within 1 minute. next_fire_at is frozen. |
| Resume | PATCH enabled: true | next_fire_at is recomputed from the current time. No backfill occurs for fires that were missed while paused. |
| Edit | PATCH cadence, active_window, starts_at, or ends_at | Changes take effect on the next expansion cycle (within ~1 minute). The current in-flight fire (if any) is not affected. |
| Delete | DELETE /schedules/{id} | Hard delete. The row, all fire history, and all pending wakeups are removed immediately. This operation is irreversible. |
Typical pause/resume flow:
// Pause
await client.schedules.update(AGENT_ID, USER_ID, scheduleId, { enabled: false });
// Resume — next_fire_at is recomputed from now
await client.schedules.update(AGENT_ID, USER_ID, scheduleId, { enabled: true });
// Delete
await client.schedules.delete(AGENT_ID, USER_ID, scheduleId);8. Preview upcoming fires
GET /api/v1/agents/{agentId}/users/{userId}/schedules/{id}/upcoming?limit=N returns the next N computed fire times as an array of UTC timestamps. The preview applies the active window, so what you see is exactly what will fire.
For example, a 4-hourly schedule (interval_hours: 4) with an 08:00–22:00 active window produces at most 4 fires per calendar day (08:00, 12:00, 16:00, 20:00 local) — not 6 (which would be the raw cadence count before filtering). The preview array reflects this.
[
"2026-05-01T00:00:00Z",
"2026-05-01T04:00:00Z",
"2026-05-01T08:00:00Z",
"2026-05-01T12:00:00Z"
]const upcoming = await client.schedules.upcoming(AGENT_ID, USER_ID, scheduleId, {
limit: 10,
});
for (const fireAt of upcoming) {
console.log(fireAt); // UTC ISO-8601 string
}9. What the agent receives
When a schedule fires, the platform constructs a structured intent block and delivers it to the agent as a proactive wakeup. The block looks like this:
[PROACTIVE WAKEUP — SCHEDULED REMINDER]
Why you're reaching out: check in on how the user is feeling
Scheduled fire time (user's local): 2026-05-02T09:00:00+08:00
Reference item (from inventory): Daily Vitamin D
dosage: 1000 IU
form: softgel
timing_notes: take with food
Additional context:
campaign: daily_checkin_v2Key points:
[PROACTIVE WAKEUP — SCHEDULED REMINDER]— the stable header the agent detects to know it is initiating a conversation, not responding to one.Why you're reaching out— verbatim content of theintentfield you set on the schedule. Write this as a short natural-language instruction to the agent. The agent composes the actual opening message in its own voice — no prompt template is exposed; you control intent, not wording.Scheduled fire time (user's local)— thenext_fire_at_localvalue at fire time. Useful for agents that want to acknowledge the time explicitly ("Good morning" vs "Good afternoon").Reference item (from inventory)— present only ifinventory_item_idwas set and the item still exists. The item'slabeland all of itspropertiesare included. Item properties are fetched live at fire time.Additional context— present only ifmetadatawas set. All metadata key-value pairs are rendered here. Use this for campaign tracking, A/B variant labels, or any additional instruction to the agent that doesn't belong in the core intent.
There is no prompt template field. Clients control agent behavior through intent, inventory_item_id, and metadata. The agent is free to adapt its tone, greeting, and language based on the user's personality and the conversation history it already has.
Error codes
| Code | Meaning |
|---|---|
CADENCE_AMBIGUOUS | Both simple and cron were provided. Exactly one is required. |
CADENCE_MISSING | Neither simple nor cron was provided. |
CADENCE_TOO_FREQUENT | Cadence resolves to more than one fire per minute. |
CADENCE_TOO_DENSE | Cadence produces more than 96 raw ticks per 24-hour window. |
INVALID_CRON | The cron expression is not a valid 5-field cron. |
INVALID_TIMEZONE | The timezone value is not a recognized IANA timezone name. |
INVALID_TIME | A value in times is not in HH:MM 24-hour format, or the time does not exist (e.g. DST gap). |
INVALID_DAY_OF_WEEK | A value in days_of_week is not one of the recognized three-letter abbreviations. |
INVALID_ACTIVE_WINDOW | active_window is structurally invalid — most commonly days_of_week: [] (explicit empty). |
INVALID_WINDOW | ends_at is not after starts_at, or ends_at is in the past. |
NO_ALLOWED_FIRE | The cadence + active_window combination produces no reachable fires in the next 90 days. |
INVENTORY_NOT_FOUND | The inventory_item_id does not exist in the user's inventory. |
Next steps
- Medication Reminders — a full worked example using Scheduled Reminders to drive a medication adherence program, including inventory schema design for medication items and multi-dose daily schedules.
- Resource Inventory + Knowledge Base — how to design inventory schemas and push live data, powering the
inventory_item_idlinkage described above. - Memory-Aware Chat — how the agent remembers user responses from previous proactive conversations and incorporates them into future interactions.