Medication Reminders
End-to-end tutorial combining Inventory + Scheduled Reminders to ship a medication adherence experience. The agent proactively reminds users to take the right drug at the right dose at the right time, in their timezone, with quiet hours respected.
This tutorial walks through a full medication-reminder implementation: define a medication entity type in your knowledge base, seed medications per user, create a Scheduled Reminder linking each medication to a cadence, and the agent proactively messages the user at the scheduled time — naming the medication and dosage in its own voice.
Tenant-agnostic primitive. The Sonzai platform has no medication-specific code. This tutorial wires two generic primitives — Inventory and Scheduled Reminders — into a medication use case. The same pattern works for watering plants, exercise reminders, bill payments, or any recurring-with-structured-data use case.
This is not a medical device. Reminders are a user-experience feature, not a clinical safety mechanism. Do not rely on Sonzai scheduled reminders as the sole adherence path for patients where missed doses cause harm.
1. Define a medication entity in your knowledge base
Create a schema for the medication entity type so the platform knows how to store and index each drug's properties. The name and ndc_code fields are indexed for fast lookup; dosage, instructions, and prescribed_by are stored but not indexed (they are fetched whole at fire time).
See the Resource Inventory + Knowledge Base tutorial for full schema semantics, field types, and upsert behaviour.
import { Sonzai } from "@sonzai-labs/agents";
const client = new Sonzai({ apiKey: process.env.SONZAI_API_KEY! });
const PROJECT_ID = "proj_abc123";
await client.knowledge.createSchema(PROJECT_ID, {
entity_type: "medication",
display_name: "Medication",
fields: [
{ name: "medication_name", type: "string", indexed: true },
{ name: "dosage", type: "string", indexed: false },
{ name: "instructions", type: "string", indexed: false },
{ name: "prescribed_by", type: "string", indexed: false },
{ name: "ndc_code", type: "string", indexed: true }, // optional; National Drug Code
],
});You only need to create the schema once per project. All subsequent medication items written for any user will be validated and indexed against this definition.
2. Seed a medication for the user
Insert one medication into the user's inventory using inventory.update with action: "add". Store the returned fact_id — you will pass it to the schedule in the next step.
const AGENT_ID = "agent_abc";
const USER_ID = "user_123";
const item = await client.agents.inventory.update(AGENT_ID, USER_ID, {
action: "add",
item_type: "medication",
label: "Ibuprofen",
description: "anti-inflammatory pain reliever, 400–500mg",
project_id: PROJECT_ID,
properties: {
medication_name: "ibuprofen",
dosage: "500mg",
instructions: "take with food",
prescribed_by: "Dr. Tan",
},
});
const inventoryItemId = item.inventory_item_id; // preferred; item.fact_id is a compat alias
// e.g. "inv_01HX8FKZQ3..."
console.log(inventoryItemId);3. Create a schedule linked to the medication
Create a twice-daily schedule at 08:00 and 20:00 Asia/Singapore, with active_window.hours set as a belt-and-braces quiet-hours guard. Pass the inventory_item_id returned in step 2. The platform will fetch the live item properties at every fire — no re-registration required when the dosage changes.
const schedule = await client.schedules.create(AGENT_ID, USER_ID, {
cadence: {
simple: { frequency: "daily", times: ["08:00", "20:00"] },
timezone: "Asia/Singapore",
},
active_window: {
hours: { start: "07:00", end: "22:00" },
},
intent: "remind the user to take their ibuprofen at the correct dose",
check_type: "reminder",
inventory_item_id: inventoryItemId,
metadata: { reminder_category: "medication" },
});
const scheduleId = schedule.schedule_id;
console.log(scheduleId); // "sched_01HX..."
console.log(schedule.next_fire_at); // "2026-05-02T00:00:00Z"
console.log(schedule.next_fire_at_local); // "2026-05-02T08:00:00+08:00"What each field controls:
| Field | Role |
|---|---|
cadence.simple.times | Wall-clock fire times in the schedule's timezone |
cadence.timezone | Per-user IANA zone; the platform does not auto-detect the user's location |
active_window.hours | Quiet-hours guard; fires computed outside the window are skipped, not deferred |
intent | The why the agent grounds its message in — written as a short natural-language instruction |
inventory_item_id | Links to the medication's structured properties, fetched live at every fire |
metadata | Opaque developer tags surfaced to the agent as "Additional context" in the wakeup block |
4. What the user sees
When the schedule fires at 08:00 Singapore time, the platform assembles a structured intent block and delivers it to the agent as a proactive wakeup. The agent composes its opening message in its own voice using the intent and the injected inventory properties. A typical output might look like:
"Morning — quick reminder, it's 8 o'clock. Time for your 500mg of ibuprofen, and remember to take it with food."
Exact wording depends on the agent's personality configuration. The agent is not given a fixed template — it receives the intent and inventory data and decides how to phrase it naturally.
Updating the dosage. When a doctor reduces the ibuprofen dose from 500mg to 250mg, update the inventory item:
await client.agents.inventory.directUpdate(AGENT_ID, USER_ID, inventoryItemId, {
properties: {
dosage: "250mg",
},
});
// No schedule edit required.
// The next scheduled fire automatically reads "250mg" from the live item.This separation is intentional: inventory is the source of truth for the what; the schedule is the source of truth for the when. They change independently. Changing the dose never touches the schedule row; moving a reminder time never touches the medication item.
5. Bounded courses (14-day antibiotic)
For medications with a fixed course length, use starts_at and ends_at to auto-disable the schedule when the course completes. Here is a 3x/day amoxicillin course that fires every 8 hours over 14 days:
const amoxItem = await client.agents.inventory.update(AGENT_ID, USER_ID, {
action: "add",
item_type: "medication",
label: "Amoxicillin",
description: "broad-spectrum antibiotic, penicillin class",
project_id: PROJECT_ID,
properties: {
medication_name: "amoxicillin",
dosage: "500mg",
instructions: "complete the full course even if you feel better",
prescribed_by: "Dr. Tan",
},
});
const amoxSchedule = await client.schedules.create(AGENT_ID, USER_ID, {
cadence: {
simple: { frequency: "interval_hours", interval_hours: 8 },
timezone: "Asia/Singapore",
},
active_window: {
hours: { start: "07:00", end: "23:00" },
},
intent: "remind the user to take their amoxicillin — emphasise completing the full course",
check_type: "reminder",
inventory_item_id: amoxItem.fact_id,
metadata: { reminder_category: "medication" },
starts_at: "2026-05-01T00:00:00Z",
ends_at: "2026-05-15T00:00:00Z",
});After ends_at passes, the schedule is automatically disabled (enabled flips to false). The inventory item for amoxicillin remains as a historical record and can be queried via the Memory API. No cleanup is required.
6. Multiple medications
Create one schedule per medication. Three daily medications = three schedules. Fires that land at the same wall-clock time produce separate proactive messages by design — each message is grounded in its own medication's inventory item.
Avoid simultaneous fires. If you want the user to receive distinct messages rather than a burst, stagger the times across schedules:
| Medication | Schedule times |
|---|---|
| Metformin | ["08:00", "20:00"] |
| Atorvastatin | ["08:15"] |
| Vitamin D | ["08:30"] |
Alternative: compose a "morning routine" item. If you prefer a single message covering all morning medications, create one inventory item of type medication_routine (define its own schema) with a medications property that lists all drugs and doses. Attach that single item to a single 08:00 schedule. The agent receives all the structured data in one wakeup block and can address all medications in a single message.
7. Track adherence (optional)
Conversational signal
When the user replies "I took it, thanks" or similar, the agent's memory layer auto-captures this as a fact on the user. You can query recent user responses to a medication reminder via the Memory API:
// Query recent memory facts mentioning medication adherence
const memories = await client.agents.memory.search(AGENT_ID, {
query: "medication taken ibuprofen",
limit: 10,
});
for (const result of memories.results) {
console.log(result.content); // "User confirmed taking 500mg ibuprofen on 2026-05-02"
console.log(result.score); // e.g. 0.91
}Explicit acknowledgement
For a harder signal, add a POST /adherence/{scheduleId} endpoint in your tenant backend that your mobile or web app calls when the user taps a confirmation button. This gives you a structured event log independent of the conversational memory layer. Sonzai does not provide this endpoint — it lives in your own backend and stores data in your own database.
8. Timezone changes when the user travels
Patch the schedule's cadence.timezone whenever the user's preferred timezone changes. Future fires are immediately recomputed in the new zone; past fire history is not modified.
// User travelling from Singapore to Los Angeles
await client.schedules.update(AGENT_ID, USER_ID, scheduleId, {
cadence: {
simple: { frequency: "daily", times: ["08:00", "20:00"] },
timezone: "America/Los_Angeles",
},
});
// Next fire: 08:00 PDT (Los Angeles) — not 08:00 SGT9. Quiet-hours for caregivers and night shifts
The active_window.hours field ensures fires outside permitted hours are silently skipped. Two common scenarios:
Caregiver setting — no overnight messages for a patient.
{
"active_window": {
"hours": { "start": "07:00", "end": "21:00" }
}
}Any cadence tick after 21:00 or before 07:00 is discarded. A twice-daily schedule with times ["08:00", "20:00"] would still fire at both times; adding a 22:00 dose would be silently skipped.
Night-shift user — active overnight, sleeping during the day.
{
"active_window": {
"hours": { "start": "22:00", "end": "06:00" }
}
}When start is greater than end, the window wraps midnight. This user receives reminders from 22:00 to 05:59 the next morning, and any cadence ticks during daytime hours are skipped.
See Scheduled Reminders — Active window for the full reference including days_of_week filtering.
Next steps
- Scheduled Reminders — full primitive reference: cadence shapes, DST handling, previewing upcoming fires, pause/resume/delete, error codes.
- Resource Inventory + Knowledge Base — KB schema depth, bulk updates,
mode="value"portfolio queries, batch import. - Memory — how the agent tracks user responses from proactive conversations and surfaces them in future interactions.
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.
Custom States & Workflow Events
Custom states let you store arbitrary structured data alongside an agent's memory — think performance metrics, task completion, milestone flags, or any application-specific context. Workflow events let the agent react to things that happen outside the conversation (milestone reached, task completed, goal achieved). By the end you'll know how to read, write, and listen for both.