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.
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.
Memory-Aware Chat
Build a conversational agent that remembers everything — user preferences, past events, commitments, and emotional context — across sessions. By the end you'll know how to seed context programmatically, search memories, and inspect what the agent has learned about a user.
Resource Inventory + Knowledge Base
Track what tools, licenses, and subscriptions each user has and enrich them with live cost data from your Knowledge Base. By the end you'll have an agent that can tell a user their total subscription spend, cost changes, and what alternative solutions to consider — all grounded in real data.