Scheduling (Universal)

One-off, daily and weekly schedule rules for Agent Notify projects — each in its own timezone, to all devices, a segment, a group or one subscriber, in any environment — with quiet hours, at-most-once dispatch and a run history. Set up from the dashboard, the chat, the API or MCP.

Agent Notify projects schedule notifications with schedule rules — one rules engine behind the dashboard's Scheduling tab, the in-dashboard agent, the API and the MCP server. A rule is:

  • when — a one-off on a date, or every day, or one day a week, at a time in the rule's own timezone;
  • what — a fixed title + body, or copy the AI writes fresh at every send (AI-Written Notifications);
  • who — all devices, a segment, a universal group or one subscriber, in one environment.

Every send goes through the ordinary universal pipeline — one inbox entry per device and analytics included.

Classic Expo apps:

The same rules serve classic apps too, with daily and weekly rules to all devices or a segment — see Scheduling. One-offs, group and subscriber audiences, environments and images are for Agent Notify projects; AI-written copy works for every project.

Three ways to set one up

  • Dashboard — Push Notifications → Scheduling: New rule opens the form (fixed or AI-written copy, cadence, audience, environment; Generate sample previews AI copy); each rule has an on/off switch, its next send, its last run and a delete. The Upcoming card lists the next sends, and Quiet hours sets the hold window. Scheduling a send from the Notifications composer also creates a one-off rule, for any audience.
  • Chat — ask the in-dashboard agent ("Every Saturday at 8:30, tell our regulars the sourdough is out"). It configures the rule, saves it off, and shows a card — Turn on schedule — so your click turns it on; an edit to a live rule shows Save changes instead. The cards are a chat-only safety step.
  • API and MCP — the routes below, or the MCP tools create_schedule_rule, update_schedule_rule and friends. They act immediately, exactly like the Scheduling tab: a rule saved with enabled: true is live, and its next occurrence sends real pushes.

One-off, daily and weekly

frequencyNeedsSends
oncedate ("YYYY-MM-DD") + timeOfDayOnce, on that date at that time.
dailytimeOfDayEvery day at that time.
weeklydayOfWeek (0 = Sunday … 6 = Saturday) + timeOfDayEvery week on that day.
  • Each rule keeps its own wall clock. timezone is an IANA zone (America/New_York), DST included: a 09:00 rule sends at 09:00 local time in July and in January. timeOfDay is 24h "HH:MM".
  • A one-off must be at least 2 minutes away when it is created, moved or switched back on (400 schedule_too_soon). Once it has gone out — or was missed — it is complete (completedAt) and can no longer be changed (409 rule_completed); create a new one instead.
  • No surprise send today. A daily or weekly rule created, resumed or rescheduled after today's time first sends at its next occurrence — nextFireIso shows exactly when.
  • The runner ticks every minute and dispatches an occurrence on the tick that reaches it (or within a 15-minute grace window, if the server was briefly busy or restarting).

Audiences

audienceKindWithReaches
all (default)—Every registered device in the rule's environment, with or without a subscriber id.
segmentsegmentIdThe members of a dashboard segment — universal subscriber ids — on every device registered under them.
groupaudienceRef = the group keyEvery device subscribed to a universal group. The group must exist when the rule is saved; a name like Church Newsletter is accepted for the key church-newsletter.
subaudienceRef = the subscriber idEvery device registered under that one subscriber id.
  • Segments over 5,000 members are refused at send time (a blocked run) — never sent in part. Split the segment, or send to all devices.
  • skipIfActiveWithinDays (1–365, segment rules only) skips subscribers with a device seen in the last N days — "they already opened the app". A universal device is seen each time the app re-registers it.
  • An audience that resolves to nobody sends nothing and records an empty run.

Environments

environment is production (default), staging or development; a rule reaches only the devices registered in it. An environment that is switched off is refused when you save the rule (400 environment_disabled) — and if it is switched off later, the rule's sends are blocked until it is back on. GET …/schedules lists the choices under environments.

Quiet hours — hold, don't drop

Quiet hours are one window per project. While they are on, an occurrence whose time falls inside the window — read in that rule's timezone — is held and sent the moment the window ends: a held run first, then the run that sends. One-offs are held the same way. Send-now pushes and classic scheduled notifications are never held.

{ "quietHoursEnabled": true, "quietHoursStart": "22:00", "quietHoursEnd": "07:00" }

The default window is 22:00–07:00. It may cross midnight, and start and end must differ. A rule's heldByQuietHours and effectiveFireIso say when a held next send will actually go out.

At most once

Every occurrence is sent at most one time — even across a restart:

  1. The runner claims the occurrence in the database before anything is sent (a one-off is marked complete by the same claim), then writes a sending run.
  2. A second runner, or the same one after a restart, finds it claimed and skips it. A run still sending 30 minutes later shows outcomeUnknown: true — the push may or may not have gone out, and it is never sent again.
  3. Each send also carries the idempotency key rule:<ruleId>:<occurrence>, so no device gets a second inbox entry for one occurrence.
  4. A one-off whose whole grace window passed while the scheduler was not running is recorded missed — nothing is sent; schedule it again.

Every outcome uses up its occurrence: a blocked or failed send is not retried — the rule simply continues at its next occurrence.

Run statuses

GET …/schedules/:ruleId/runs returns a rule's 20 most recent runs, newest first.

statusMeaning
sentThe push went out. audienceCount = devices targeted; title / body are the copy that was sent; sendId links it to the inbox and analytics.
heldQuiet hours held it; a later run records the send.
emptyNobody to send to (no members, no registered device, or everyone skipped) — nothing sent.
blockedA gate refused it: an expired trial or membership, a disabled environment, or a segment over 5,000 members. detail says which.
errorThe send failed — for example the segment was deleted, or devices matched but no transport accepted the push (missing credentials).
failedAn AI-written rule's copy could not be written — nothing was sent. See AI-Written Notifications.
missedA one-off whose time passed while the scheduler was not running — nothing was sent.
sendingClaimed and in flight. outcomeUnknown: true when it never settled.

API

The routes are account routes: call them with a dashboard session or an MCP access token (Authorization: Bearer nnmcp_…) — an app token is refused with 403. Every call re-checks that the account (or its team) can reach the app (404 app_not_found otherwise). Any member of the app's team may manage rules, the same as on the Scheduling tab.

GET    https://app.nativenotify.com/api/agent/apps/:appId/schedules                — rules + quiet hours
GET    https://app.nativenotify.com/api/agent/apps/:appId/schedules/upcoming       — the next sends
POST   https://app.nativenotify.com/api/agent/apps/:appId/schedules                — create a rule
PATCH  https://app.nativenotify.com/api/agent/apps/:appId/schedules/:ruleId        — update / pause / resume
DELETE https://app.nativenotify.com/api/agent/apps/:appId/schedules/:ruleId        — delete a rule
GET    https://app.nativenotify.com/api/agent/apps/:appId/schedules/:ruleId/runs   — run history
GET    https://app.nativenotify.com/api/agent/apps/:appId/schedules/settings       — quiet hours
PUT    https://app.nativenotify.com/api/agent/apps/:appId/schedules/settings       — set quiet hours
POST   https://app.nativenotify.com/api/agent/apps/:appId/schedules/ai-sample      — preview AI copy

Create a rule

A one-off to the church newsletter group, the Saturday before the bake sale:

{
  "name": "Harvest bake sale",
  "enabled": true,
  "title": "Bake sale this Saturday",
  "body": "Pies, bread and cookies in the church hall from 9:00. Everything goes to the food pantry.",
  "frequency": "once",
  "date": "2026-10-03",
  "timeOfDay": "08:00",
  "timezone": "America/New_York",
  "audienceKind": "group",
  "audienceRef": "church-newsletter",
  "pushData": { "url": "/events/bake-sale" }
}

A weekly rule to a segment, skipping anyone who opened the app in the last 3 days, saved paused:

{
  "name": "Saturday bread",
  "enabled": false,
  "title": "Fresh sourdough",
  "body": "Loaves come out of the oven at 8:30.",
  "frequency": "weekly",
  "dayOfWeek": 6,
  "timeOfDay": "08:30",
  "timezone": "America/Chicago",
  "audienceKind": "segment",
  "segmentId": 4,
  "skipIfActiveWithinDays": 3
}
FieldRequiredNotes
nameyesUp to 120 characters.
enablednoDefault true — the rule is live unless you pass false.
contentModeno"fixed" (default) or "ai" — see AI-Written Notifications.
title, bodyfixed copyUp to 200 / 4,000 characters.
appDescription, instructions, language, examplesAI copyThe brief — see AI-Written Notifications.
frequencyyes"once", "daily" or "weekly".
dateonce"YYYY-MM-DD", in the rule's timezone.
dayOfWeekweekly0–6, 0 = Sunday.
timeOfDayyes24h "HH:MM".
timezoneyesIANA zone, up to 60 characters.
audienceKindno"all" (default), "segment", "group" or "sub".
segmentIdsegmentA segment of this project.
audienceRefgroup, subThe group key or the subscriber id, up to 256 characters.
environmentno"production" (default), "staging" or "development".
skipIfActiveWithinDaysno1–365 or null; segment rules only.
pushDatanoA JSON object (or JSON string) delivered with every send, up to 16 KiB.
bigPictureURLnoAn http(s) image url, up to 2,000 characters.

Response — 201: the saved rule plus a timing hint that compares its next send with the project's best send times (best.dow / best.hour are the UTC weekday and hour with the most notification opens in the last 90 days; nearBest is true when the next send lands on that weekday within two hours of it):

{
  "ok": true,
  "timing": {
    "hasData": true,
    "totalOpens": 412,
    "best": { "dow": 6, "hour": 13, "opened": 37 },
    "nearBest": true,
    "cadenceChecked": { "frequency": "once", "dayOfWeek": null, "timeOfDay": "08:00" }
  },
  "rule": {
    "id": 42,
    "name": "Harvest bake sale",
    "enabled": true,
    "audienceKind": "group",
    "segmentId": null,
    "segmentName": null,
    "title": "Bake sale this Saturday",
    "body": "Pies, bread and cookies in the church hall from 9:00. Everything goes to the food pantry.",
    "pushData": { "url": "/events/bake-sale" },
    "frequency": "once",
    "dayOfWeek": null,
    "timeOfDay": "08:00",
    "timezone": "America/New_York",
    "skipIfActiveWithinDays": null,
    "cadence": "Once · Sat, Oct 3, 2026 · 08:00",
    "lastFiredAt": null,
    "lastFiredLocalDate": null,
    "pendingFireAt": null,
    "nextFireIso": "2026-10-03T12:00:00.000Z",
    "nextFireLocal": "Sat, Oct 3 · 08:00",
    "heldByQuietHours": false,
    "effectiveFireIso": "2026-10-03T12:00:00.000Z",
    "skipIfActiveApplies": false,
    "createdAt": "2026-09-25T16:20:00.000Z",
    "updatedAt": "2026-09-25T16:20:00.000Z",
    "lastRun": null,
    "contentMode": "fixed",
    "ai": null,
    "audienceRef": "church-newsletter",
    "audienceLabel": "Group “church-newsletter”",
    "onceDate": "2026-10-03",
    "completedAt": null,
    "environment": "production",
    "bigPictureURL": null
  }
}
  • id is the rule's stable id for every other call.
  • nextFireIso is the next send in UTC (null while paused or once complete); nextFireLocal is the same moment on the rule's clock.
  • lastRun is the newest run (status, firedAt, audienceCount, detail, title, body, sendId).
  • completedAt is set once a one-off has gone out or was missed.

List, update, delete

  • GET …/schedules — { ok, pipeline, environments, settings, rules }: every rule in the shape above, the quiet-hours settings, pipeline: "universal" for an Agent Notify project, and environments — { default, choices: [{ key, label, devices }], showPicker }: the enabled environments that have registered devices (production always, while it is enabled).
  • GET …/schedules/upcoming — { ok, upcoming }: the 10 soonest sends across enabled rules (kind: "rule" with ruleId, whenIso, localText, frequency, contentMode, heldByQuietHours, effectiveFireIso), merged with any pending classic scheduled notifications (kind: "scheduled"). An AI-written rule has no title here — it is written at send time.
  • PATCH …/schedules/:ruleId — send only what changes; every other field keeps its stored value. { "enabled": false } pauses a rule, { "enabled": true } resumes it. null clears pushData, bigPictureURL, skipIfActiveWithinDays or instructions. Answers { ok, rule }.
  • DELETE …/schedules/:ruleId — { ok, deleted: { id, name } }. The rule's run history is deleted with it; what it already sent stays in inboxes and analytics. Deleting a one-off that has not gone out cancels it.
  • PUT …/schedules/settings — { quietHoursEnabled, quietHoursStart, quietHoursEnd } (send only what changes); answers { ok, settings }.

Errors

Errors use the envelope { "error": { "code", "message", "field"? } }:

StatusCodeMeaning
400bad_scheduleA rule field is missing or invalid — the message names it (for example timeOfDay must be a 24h time like "09:00", or That group does not exist on this project).
400schedule_too_soonA one-off less than 2 minutes away.
400invalid_environment, environment_disabledNot one of the three environments, or switched off for this project.
400bad_app_id, bad_rule_id, bad_settingsA malformed id, or invalid quiet hours.
404app_not_found, rule_not_foundThe app is not one this account can access, or the rule is not on it.
409rule_completedThe one-off already went out (or was missed).

An app token instead of an account credential answers 403 with a plain-text message.

For agents

The MCP server wraps every route:

ToolRoute
list_schedule_rulesGET …/schedules
list_upcoming_scheduled_sendsGET …/schedules/upcoming
list_schedule_rule_runsGET …/schedules/:ruleId/runs
create_schedule_rulePOST …/schedules
update_schedule_rulePATCH …/schedules/:ruleId
delete_schedule_ruleDELETE …/schedules/:ruleId
set_quiet_hoursPUT …/schedules/settings
generate_ai_schedule_samplePOST …/schedules/ai-sample
list_segmentsGET /api/agent/apps/:appId/segments — the ids for segment audiences

The tools take snake_case arguments (time_of_day, audience_ref, …), accept a weekday name for day_of_week and times like "9:00 AM", and create_schedule_rule requires enabled — there is no default, because an enabled rule sends real pushes. Agents are instructed to confirm the copy, audience and timing with you before they turn a rule on.