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_ruleand friends. They act immediately, exactly like the Scheduling tab: a rule saved withenabled: trueis live, and its next occurrence sends real pushes.
One-off, daily and weekly
frequency | Needs | Sends |
|---|---|---|
once | date ("YYYY-MM-DD") + timeOfDay | Once, on that date at that time. |
daily | timeOfDay | Every day at that time. |
weekly | dayOfWeek (0 = Sunday … 6 = Saturday) + timeOfDay | Every week on that day. |
- Each rule keeps its own wall clock.
timezoneis an IANA zone (America/New_York), DST included: a 09:00 rule sends at 09:00 local time in July and in January.timeOfDayis 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 —
nextFireIsoshows 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
audienceKind | With | Reaches |
|---|---|---|
all (default) | — | Every registered device in the rule's environment, with or without a subscriber id. |
segment | segmentId | The members of a dashboard segment — universal subscriber ids — on every device registered under them. |
group | audienceRef = the group key | Every 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. |
sub | audienceRef = the subscriber id | Every device registered under that one subscriber id. |
- Segments over 5,000 members are refused at send time (a
blockedrun) — 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
emptyrun.
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:
- 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
sendingrun. - A second runner, or the same one after a restart, finds it claimed and skips it. A run still
sending30 minutes later showsoutcomeUnknown: true— the push may or may not have gone out, and it is never sent again. - Each send also carries the idempotency key
rule:<ruleId>:<occurrence>, so no device gets a second inbox entry for one occurrence. - 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.
status | Meaning |
|---|---|
sent | The push went out. audienceCount = devices targeted; title / body are the copy that was sent; sendId links it to the inbox and analytics. |
held | Quiet hours held it; a later run records the send. |
empty | Nobody to send to (no members, no registered device, or everyone skipped) — nothing sent. |
blocked | A gate refused it: an expired trial or membership, a disabled environment, or a segment over 5,000 members. detail says which. |
error | The send failed — for example the segment was deleted, or devices matched but no transport accepted the push (missing credentials). |
failed | An AI-written rule's copy could not be written — nothing was sent. See AI-Written Notifications. |
missed | A one-off whose time passed while the scheduler was not running — nothing was sent. |
sending | Claimed 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
}
| Field | Required | Notes |
|---|---|---|
name | yes | Up to 120 characters. |
enabled | no | Default true — the rule is live unless you pass false. |
contentMode | no | "fixed" (default) or "ai" — see AI-Written Notifications. |
title, body | fixed copy | Up to 200 / 4,000 characters. |
appDescription, instructions, language, examples | AI copy | The brief — see AI-Written Notifications. |
frequency | yes | "once", "daily" or "weekly". |
date | once | "YYYY-MM-DD", in the rule's timezone. |
dayOfWeek | weekly | 0–6, 0 = Sunday. |
timeOfDay | yes | 24h "HH:MM". |
timezone | yes | IANA zone, up to 60 characters. |
audienceKind | no | "all" (default), "segment", "group" or "sub". |
segmentId | segment | A segment of this project. |
audienceRef | group, sub | The group key or the subscriber id, up to 256 characters. |
environment | no | "production" (default), "staging" or "development". |
skipIfActiveWithinDays | no | 1–365 or null; segment rules only. |
pushData | no | A JSON object (or JSON string) delivered with every send, up to 16 KiB. |
bigPictureURL | no | An 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
}
}
idis the rule's stable id for every other call.nextFireIsois the next send in UTC (nullwhile paused or once complete);nextFireLocalis the same moment on the rule's clock.lastRunis the newest run (status,firedAt,audienceCount,detail,title,body,sendId).completedAtis 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-hourssettings,pipeline: "universal"for an Agent Notify project, andenvironments—{ 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"withruleId,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.nullclearspushData,bigPictureURL,skipIfActiveWithinDaysorinstructions. 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"? } }:
| Status | Code | Meaning |
|---|---|---|
400 | bad_schedule | A 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). |
400 | schedule_too_soon | A one-off less than 2 minutes away. |
400 | invalid_environment, environment_disabled | Not one of the three environments, or switched off for this project. |
400 | bad_app_id, bad_rule_id, bad_settings | A malformed id, or invalid quiet hours. |
404 | app_not_found, rule_not_found | The app is not one this account can access, or the rule is not on it. |
409 | rule_completed | The 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:
| Tool | Route |
|---|---|
list_schedule_rules | GET …/schedules |
list_upcoming_scheduled_sends | GET …/schedules/upcoming |
list_schedule_rule_runs | GET …/schedules/:ruleId/runs |
create_schedule_rule | POST …/schedules |
update_schedule_rule | PATCH …/schedules/:ruleId |
delete_schedule_rule | DELETE …/schedules/:ruleId |
set_quiet_hours | PUT …/schedules/settings |
generate_ai_schedule_sample | POST …/schedules/ai-sample |
list_segments | GET /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.
Related
- AI-Written Notifications — fresh copy at every send, and the sample endpoint.
- Inbox & Audience — what each send reached, and who is registered.
- Send Notifications — the same audiences, sent now.
- Groups and Environments.