Skip to main content
SONZAI

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_hours course constrained by starts_at and ends_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

FieldTypeRequiredDescription
frequency"daily" | "weekly" | "interval_hours"YesRecurrence pattern
timesstring[]Yes for daily/weeklyWall-clock times in HH:MM (24-hour), evaluated in the schedule's timezone
days_of_weekstring[]Yes for weekly"mon", "tue", "wed", "thu", "fri", "sat", "sun"
interval_hoursnumberYes for interval_hoursMinimum 1, maximum 24
timezoneIANA stringYesApplied 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

FieldTypeRequiredDescription
expressionstringYesStandard 5-field cron (min hour dom month dow)
timezoneIANA stringYesCron 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 (enabled flips to false). 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

OperationHowBehavior
PausePATCH enabled: falseSchedule stops producing fires within 1 minute. next_fire_at is frozen.
ResumePATCH enabled: truenext_fire_at is recomputed from the current time. No backfill occurs for fires that were missed while paused.
EditPATCH cadence, active_window, starts_at, or ends_atChanges take effect on the next expansion cycle (within ~1 minute). The current in-flight fire (if any) is not affected.
DeleteDELETE /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_v2

Key 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 the intent field 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) — the next_fire_at_local value at fire time. Useful for agents that want to acknowledge the time explicitly ("Good morning" vs "Good afternoon").
  • Reference item (from inventory) — present only if inventory_item_id was set and the item still exists. The item's label and all of its properties are included. Item properties are fetched live at fire time.
  • Additional context — present only if metadata was 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

CodeMeaning
CADENCE_AMBIGUOUSBoth simple and cron were provided. Exactly one is required.
CADENCE_MISSINGNeither simple nor cron was provided.
CADENCE_TOO_FREQUENTCadence resolves to more than one fire per minute.
CADENCE_TOO_DENSECadence produces more than 96 raw ticks per 24-hour window.
INVALID_CRONThe cron expression is not a valid 5-field cron.
INVALID_TIMEZONEThe timezone value is not a recognized IANA timezone name.
INVALID_TIMEA value in times is not in HH:MM 24-hour format, or the time does not exist (e.g. DST gap).
INVALID_DAY_OF_WEEKA value in days_of_week is not one of the recognized three-letter abbreviations.
INVALID_ACTIVE_WINDOWactive_window is structurally invalid — most commonly days_of_week: [] (explicit empty).
INVALID_WINDOWends_at is not after starts_at, or ends_at is in the past.
NO_ALLOWED_FIREThe cadence + active_window combination produces no reachable fires in the next 90 days.
INVENTORY_NOT_FOUNDThe 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_id linkage described above.
  • Memory-Aware Chat — how the agent remembers user responses from previous proactive conversations and incorporates them into future interactions.

On this page