# AI-Written Notifications

Schedule rules whose copy is written fresh at every send from a short brief — what the app is, standing instructions, language and example notifications — checked before it goes out, never repeating recent sends, and never sent blank. Preview it first with the sample endpoint.

A [schedule rule](/docs/universal-push/scheduling) with `contentMode: "ai"` carries a **brief** instead of a fixed title and body. At every send, Native Notify writes one new notification from the brief, checks it, and sends it to the rule's audience like any other scheduled send — so a weekly reminder never reads the same twice.

It works for **every project**: Agent Notify projects (any cadence, audience and environment a rule takes) and classic Expo apps (daily and weekly rules to all devices or a segment). The older classic [AI Push](/docs/scheduling#ai-push) schedules are a separate API and keep working unchanged.

## The brief

| Field            | Required | Limit                            | Notes                                                                                                                                     |
| ---------------- | -------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `appDescription` | yes      | 2,000 characters                 | What the app is and what the notifications should be about. Put every fact you want mentioned here — times, places, what is new.          |
| `instructions`   | no       | 2,000 characters                 | Standing rules for the writer: tone, topics to avoid, calls to action.                                                                    |
| `language`       | no       | 60 characters                    | The language to write in. Default `"English"`.                                                                                            |
| `examples`       | no       | 10 examples, 300 characters each | Example notifications for the voice — a list of strings, or one string with one example per line. They set the tone and are never copied. |

A weekly AI-written rule for a bookstore, saved paused so you can preview it first:

```json
{
  "name": "Saturday story hour",
  "enabled": false,
  "contentMode": "ai",
  "appDescription": "Maple Street Books, a family bookstore. Invite families to Saturday story hour at 10:00 in the children's corner, and mention that new picture books arrive every week.",
  "instructions": "Warm and brief. No prices or discounts. Always say story hour starts at 10:00.",
  "language": "English",
  "examples": [
    "Story hour starts at 10 — bring the little ones!",
    "New picture books just arrived in the children's corner."
  ],
  "frequency": "weekly",
  "dayOfWeek": 5,
  "timeOfDay": "17:00",
  "timezone": "America/Chicago",
  "audienceKind": "all"
}
```

Send it to `POST /api/agent/apps/:appId/schedules` — every other field (cadence, audience, environment, `pushData`, quiet hours) works exactly as on [Scheduling](/docs/universal-push/scheduling). An AI rule stores no title or body: in the rule list it has `title: null`, `body: null` and its brief under `ai`, and each run records the copy that actually went out.

## What happens at each send

1. The runner claims the occurrence — [at most once](/docs/universal-push/scheduling#at-most-once), like every rule.
2. It asks the model for **one** title and body, handing it the brief and the rule's 10 most recent sent notifications, so it varies the angle and wording instead of repeating itself.
3. The answer is checked: a non-empty title of at most **65** characters and a non-empty body of at most **180** (an emoji counts once), with markup, markdown and wrapping quotes removed. An unusable answer gets one corrective retry.
4. Usable copy is sent to the rule's audience and saved on the run (`title`, `body`) — which is also the history step 2 reads next time.
5. **No usable copy means nothing is sent.** If the model cannot be reached, or both answers are unusable, the run is recorded `failed` with the reason, and the rule tries again at its next occurrence. There is never a blank, placeholder or fallback push.

## The writing rules

Every AI-written notification follows the same rules, whatever the brief says:

- Modest, kind and appropriate for the whole family — nothing crude, suggestive, violent, hateful or mocking.
- Nothing about gambling, alcohol, drugs, tobacco, the occult or party culture.
- No fear, guilt or pressure tactics.
- **No invented facts** — no prices, discounts, dates, deadlines or promises the brief does not state.
- Plain text in the brief's language, one clear idea, at most one emoji.
- The owner's standing `instructions` are followed unless they conflict with these rules.

## Preview a sample

```text
POST https://app.nativenotify.com/api/agent/apps/:appId/schedules/ai-sample
```

Writes **one** sample from a brief — **nothing is saved and nothing is sent**. Send the brief fields, or `ruleId` of an existing AI rule: its stored brief fills the fields you leave out, and its recent sends count as the no-repeat history, exactly as at send time.

```json
{
  "appDescription": "Maple Street Books, a family bookstore. Invite families to Saturday story hour at 10:00.",
  "instructions": "Warm and brief. No prices or discounts.",
  "language": "English"
}
```

Response — `200`:

```json
{
  "ok": true,
  "sample": {
    "title": "Story hour this Saturday",
    "body": "Bring the family to the children's corner at 10:00 for stories and brand-new picture books."
  },
  "model": "~deepseek/deepseek-flash-latest",
  "attempts": 1
}
```

`attempts` is `2` when the corrective retry was needed. Every sample is a real model call, so each account user can write **20 samples a minute and 300 a day** (the daily count resets at midnight UTC).

| Status | Code                              | Meaning                                                                                        |
| ------ | --------------------------------- | ---------------------------------------------------------------------------------------------- |
| `400`  | `bad_brief`                       | The brief is invalid — missing `appDescription`, or a field over its limit (`field` names it). |
| `400`  | `bad_rule_id`                     | `ruleId` is not a positive integer.                                                            |
| `404`  | `app_not_found`, `rule_not_found` | The app is not one this account can access, or the rule is not on it.                          |
| `429`  | `limit_reached`                   | The per-minute or per-day sample limit is used up.                                             |
| `502`  | `llm_failed`, `invalid_output`    | The model could not be reached, or returned no usable copy after the retry.                    |
| `503`  | `llm_not_configured`              | AI writing is not configured on the server.                                                    |

The same call is behind the dashboard's **Generate sample** button (the rule form) and **Sample** button (a saved AI rule), and the chat always writes a sample before it saves an AI rule — the **Turn on schedule** card shows it, and a brief that cannot produce usable copy saves nothing.

## Saving and editing

- `contentMode: "ai"` needs `appDescription` (or a stored brief, on an update); a missing or oversized field answers `400 bad_schedule`, and the error's `field` names it.
- `title` and `body` are ignored for an AI rule. Switching a fixed rule to AI clears its fixed title and body — switching back needs them again. Switching back to fixed keeps the brief stored, so it is still there if you switch to AI later.
- `PATCH` changes only the fields you send: `{ "instructions": null }` clears the instructions, `{ "examples": [] }` clears the examples.

## For agents

The [MCP server](/docs/mcp-server) has `generate_ai_schedule_sample` (the sample endpoint — pass `app_description`, or `rule_id`), and `create_schedule_rule` / `update_schedule_rule` take `content_mode: "ai"` with `app_description`, `instructions`, `language` and `examples`. A good agent flow: write a sample, show it to the user, adjust the brief, and only then create the rule with `enabled: true` — or save it with `enabled: false` and let the user turn it on.

## Related

- [Scheduling](/docs/universal-push/scheduling) — cadence, audiences, quiet hours and run statuses.
- [Inbox & Audience](/docs/universal-push/inbox-and-audience) — what each send reached and who read it.
