# Notification Manager

> Cron infrastructure and reminder lifecycle management for the AI weight loss companion. Creates, syncs, and removes meal/weight reminder cron jobs. Manages the engagement lifecycle (Active → Pause → Recall → Silent). Handles adaptive timing, user reminder setting changes, and leave/vacation management. Use this skill when: meal-planner completes onboarding (to bootstrap reminders), user requests…

- **Type:** Skill
- **Install:** `agentstack add skill-nanorhino-weight-loss-skill-notification-manager`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [NanoRhino](https://agentstack.voostack.com/s/nanorhino)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [NanoRhino](https://github.com/NanoRhino)
- **Source:** https://github.com/NanoRhino/weight-loss-skill/tree/main/notification-manager
- **Website:** https://nanorhino.com/

## Install

```sh
agentstack add skill-nanorhino-weight-loss-skill-notification-manager
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Notification Manager

> ⚠️ **SILENT OPERATION:** Never narrate internal actions, skill transitions, or tool calls to the user. No "Let me check...", "Now I'll transition to...", "Reading your profile...". Just do it silently and respond with the result.

Orchestration layer for reminders — cron CRUD, lifecycle management, adaptive
timing, and setting changes. This skill decides **when** to send and **whether
to keep sending**. The actual message content is composed by `notification-composer`.

## Cron Infrastructure

### Script

All cron job creation must go through this skill's script:

```bash
bash {baseDir}/scripts/create-reminder.sh
```

This script (migrated from the former `scheduled-reminders` skill) auto-resolves
delivery config for multiple channels (Slack, WeChat, WeCom, etc.).

### One-shot reminder

```bash
bash {baseDir}/scripts/create-reminder.sh \
  --agent  \
  --channel  \
  --name "Descriptive name" \
  --message "Reminder content" \
  --at "2m"
```

`--at` accepts relative time (`2m`, `1h`, `30s`) or ISO timestamp (`2026-03-04T10:00:00Z`).
One-shot reminders auto-delete after running. Use `--keep` to preserve them.

### Recurring reminder

```bash
bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --name "Lunch reminder" \
  --message "Run notification-composer for lunch." \
  --cron "0 12 * * *"
```

`--tz` auto-detects from the agent workspace's `USER.md`. Falls back to `Asia/Shanghai` if not found. You can override explicitly with `--tz`.

### Parameters

| Param | Required | Description |
|-------|----------|-------------|
| `--agent` | ✅ | Your agent ID (e.g. `wechat-dm-xxx`, `007-zhuoran`) |
| `--channel` | ❌ | Delivery channel (`wechat`, `wecom`, `slack`, etc.). Defaults to `slack` if omitted (backward-compatible) |
| `--name` | ✅ | Descriptive job name (shown in cron list) |
| `--message` | ✅ | Prompt sent to user when the job fires |
| `--at` | one of | One-shot: relative time or ISO timestamp |
| `--cron` | one of | Recurring: 5-field cron expression |
| `--tz` | ❌ | Timezone for cron (auto-detects from `timezone.json`, fallback: `Asia/Shanghai`) |
| `--keep` | ❌ | Don't auto-delete one-shot jobs after running |
| `--to` | ❌ | Explicit delivery target. Overrides auto-detection. Required for channels other than `slack`/`wechat`/`wecom` |
| `--type` | ❌ | Job type for anti-burst scheduling: `meal`, `weight`, or `other` (default: `other`). See **Anti-burst scheduling** below |
| `--exact` | ❌ | Skip anti-burst logic, use exact cron time. Use for time-sensitive reminders that must fire at the precise minute |

### How it works

The script resolves the delivery target (`--to`) based on the channel:

| Channel | Auto-detection | Example |
|---------|---------------|---------|
| `slack` (default) | Looks up Slack user ID from `~/.openclaw/openclaw.json` bindings → `user:` | `--agent 007-zhuoran` → `user:U12345` |
| `wechat` / `wecom` | Extracts userId from agent ID (`wechat-dm-xxx` → `xxx`) | `--agent wechat-dm-abc123` → `abc123` |
| Others | No auto-detection — must pass `--to` explicitly | `--to "123456789"` |

Timezone auto-detection searches these paths in order:
1. `~/.openclaw/workspace-$AGENT/USER.md`
2. `~/.openclaw/workspace-nutritionist/$AGENT/USER.md`

Then calls `openclaw cron add` with `sessionTarget = "isolated"`, `payload.kind = "agentTurn"`, and `delivery.mode = "announce"` automatically. The isolated agent composes the reminder and outputs the text — announce delivery sends it to the user and automatically injects the context into the main session.

### Anti-burst scheduling

When creating **recurring** cron jobs (`--cron`), the script automatically avoids
scheduling too many jobs at the same minute to prevent bulk message sends that
could trigger platform rate limits or account bans.

**How it works:**
1. Fetches all existing recurring cron jobs from the gateway
2. Converts all cron times to UTC for cross-timezone comparison
3. Checks if the target minute already has ≥2 jobs
4. If full, scans nearby minutes for an available slot ( Meal Schedule`:

1. List existing reminder cron jobs (`action: "list"`).
2. Derive the expected cron times from `health-profile.md > Meal Schedule` (each meal time minus 15 min).
3. Compare:
   - **Missing jobs** (expected time has no matching cron) → create them.
   - **Stale jobs** (cron exists but its time doesn't match any current meal time) → remove then recreate.
   - **Legacy jobs** (cron exists and time matches, but `--message` references `daily-notification` or `daily-notification-skill` instead of `notification-composer`) → remove then recreate with the correct `notification-composer` message. This ensures old cron jobs from before the skill split are automatically migrated.
   - **Matching jobs** (time matches AND message references `notification-composer`) → no action.
4. Also verify weight reminder cron jobs exist — see § "Weight reminders" below. This includes the primary (Wed & Sat morning) and next-morning followup (Thu & Sun morning). Create any that are missing.
5. Also verify the weekly report cron job exists (Sunday 21:00 — see § "Weekly report" below). Create if missing.
6. **Diet pattern detection** — special handling:
   - Read `health-profile.md > Automation > Pattern Detection Completed`
   - If has a date → job already completed. If job still exists, remove it (stale).
   - If `—` (not completed) → check if job exists. If missing → create it.
7. Do all of this **silently** — do not mention it to the user.

**When creating multiple jobs at once** (initial bootstrap or large sync), use `batch-create-reminders.sh` instead of calling `create-reminder.sh` one by one. It handles slot allocation in a single pass and creates all jobs in parallel:

```bash
bash {baseDir}/scripts/batch-create-reminders.sh \
  --agent  \
  --channel  \
  --workspace {workspaceDir} \
  --skip-existing
```

Use `--only meal,weight,report,pattern` to restrict which job types are created. The `--skip-existing` flag prevents duplicate creation during partial syncs.

---

## Cron Job Definitions

Create recurring cron jobs using the script above. Derive the cron times from `health-profile.md > Meal Schedule` (each meal time minus 15 min). **Do NOT pass `--tz`** — the script auto-detects from `USER.md`. **Pass `--channel`** to match the agent's delivery channel (e.g. `wechat`, `slack`). If omitted, defaults to `slack` for backward compatibility.

> ⚠️ **Cron expressions use the user's LOCAL time. Do NOT convert to UTC.** The script sets `--tz` automatically, so the cron scheduler handles timezone conversion. Example: if meal is at 09:00 Beijing time, the cron expression is `45 8 * * *` (08:45 local), NOT `45 0 * * *`.

Every meal cron `--message` MUST tell the agent to run `notification-composer` for that meal. Keep it minimal — notification-composer owns pre-send checks, message composition, and reply handling. Do not duplicate its rules in the cron message.

**Meal naming:** Always use standard meal names (`breakfast`, `lunch`, `dinner`) — never `meal_1`/`meal_2`. For 2-meal users, use whichever two standard names match their schedule (e.g., user eats at 12:00 and 18:30 → `lunch` and `dinner`).

```bash
# Example: 3 meals, reminders 15 min before each (adjust times from health-profile.md)
# Note: --type meal ensures anti-burst scheduling with [-10, +5] min window
bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --type meal --name "Breakfast reminder" \
  --message "Run notification-composer for breakfast." \
  --cron "45 6 * * *"

bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --type meal --name "Lunch reminder" \
  --message "Run notification-composer for lunch." \
  --cron "45 11 * * *"

bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --type meal --name "Dinner reminder" \
  --message "Run notification-composer for dinner." \
  --cron "45 17 * * *"

# Example: 2 meals (lunch + dinner)
bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --type meal --name "Lunch reminder" \
  --message "Run notification-composer for lunch." \
  --cron "45 11 * * *"

bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --type meal --name "Dinner reminder" \
  --message "Run notification-composer for dinner." \
  --cron "15 18 * * *"
```

### Weight reminders (2x/week + followups)

> ⚠️ **Breakfast fallback:** If user has no breakfast (BREAKFAST_TIME is empty/null), use the **earliest meal time** from `health-profile.md > Meal Schedule` as the reference for all "breakfast time − 30 min" calculations below. For example, if user only eats lunch (12:00) and dinner (18:00), weight primary reminder = 11:30, morning followup = 11:30. The condition for creating weight reminders is that **at least one meal time exists** — not specifically breakfast.

**Primary reminder:** Cron time = breakfast time (or earliest meal) minus **30 min**. Fires Wed & Sat.

```bash
# Example assumes breakfast at 07:00 → weight cron at 06:30
bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --type weight --name "Weight check-in reminder" \
  --message "Run notification-composer for weight." \
  --cron "30 6 * * 3,6"
```

**Next-morning followup:** Cron time = breakfast time (or earliest meal) minus **30 min**. Fires Thu & Sun (day after primary). Only sends if the user did NOT weigh in yesterday OR today. Pre-send-check uses `weight_morning_followup` type.

```bash
# Example assumes breakfast at 07:00 → morning followup at 06:30
bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --type weight --name "Weight morning followup" \
  --message "Run notification-composer for weight_morning_followup." \
  --cron "30 6 * * 4,0"
```

### Weekly report (Sunday 9 PM)

One fixed cron job — every Sunday at 21:00 user local time.

```bash
bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --name "Weekly report" \
  --message "🚨 WEEKLY REPORT — MANDATORY SCRIPT EXECUTION\n\nGenerate this week's weekly report using the weekly-report skill.\n\nABSOLUTE RULES:\n1. Run collect-weekly-data.py to gather all nutrition/weight/exercise data\n2. Run generate-report-html.py with real commentary/highlights/suggestions — capture the URL from stdout\n3. The final message to user MUST contain the clickable report URL\n4. If any script fails, report the error — do NOT fall back to a text-only summary\n5. A delivery without a report URL = FAILED execution\n\n❌ FORBIDDEN: Writing a text summary without running the scripts\n❌ FORBIDDEN: Sending a message without a report link (https://nanorhino.ai/user/...)\n✅ REQUIRED: The message MUST contain the actual uploaded report URL\n\nSkill: weekly-report\nUser workspace: {workspaceDir}" \
  --cron "0 21 * * 0"
```

---

### Diet pattern detection (self-destructing, onboarding + 3 days)

One-time diet pattern analysis. Created at onboarding, starts running 3 days after `Onboarding Completed` date (from `health-profile.md > Automation`). Cron time = dinner + 3h.

```bash
bash {baseDir}/scripts/create-reminder.sh \
  --agent  --channel  --name "Diet pattern detection" \
  --message "Run diet-pattern-detection skill." \
  --cron "0 21 * * *"
```

**Not included in normal auto-sync** — this job is managed by its own lifecycle:
- Created once at onboarding (by notification-manager)
- Self-deleted by diet-pattern-detection skill after successful execution
- See auto-sync special handling below

---

### First-meal nudge (one-shot, activation flow)

**Purpose:** Break the ice for users who completed onboarding (picked a plan,
set meal reminders) but have **never logged a single meal**. The stall point is
"now actually text me your food." This is a gentle, capped nudge — NOT a recall.

**Created at onboarding completion** by `batch-create-reminders.sh` (included in
the default `--only all` bootstrap; restrict with `--only firstmeal`). It
creates **two one-shot** (`--at`) jobs:

| Job name | Fires | Payload |
|----------|-------|---------|
| `First meal nudge` | ~3-4h after completion, at the next meal slot today (daytime-capped 08:00-20:00 local) — else first meal slot next day | `... pre-send-check.py --meal-type first_meal_nudge ...` then `notification-composer for first_meal_nudge (nudge=1)` |
| `First meal nudge followup` | First meal slot the following day (softer) | same, `nudge=2` |

**Timing uses the user's IANA timezone** (from `USER.md > Timezone`). The nudge
fires **at** the meal slot, deliberately offset from the meal-reminder minutes
(which fire at slot−15min), so the user never gets the nudge and a meal reminder
minutes apart.

**Detection (who gets it):** `health-profile.md` has a real
`Onboarding Completed` date AND `data/meals/` has zero food entries AND
`## Meal Schedule` is populated (so meal crons exist). The one-shot crons are
created unconditionally at onboarding; the actual send is gated at fire time by
`pre-send-check.py --meal-type first_meal_nudge`, which self-cancels if
onboarding is NOT completed (wrong cohort — keeps it mutually exclusive with the
activation nudge), the user logged any meal, is on leave, or the cap is reached.

**Cap & terminal state:** Max **2** nudges (`activation.first_meal_nudges_sent`,
a non-stage business counter in `data/engagement.json`). The moment the user logs
ANY meal, both nudges self-cancel (pre-send-check). After 2 nudges, the
pre-send-check **cap gate** (reads `activation.first_meal_nudges_sent >= 2`)
permanently suppresses the nudge — this is the terminal anti-nag guarantee and is
**lifecycle-independent** (it does not depend on the stage system). The intent is
that a never-logged user never receives the engaged-recall content (S2-S4), which
is generated from logged meals and would be hollow.

> **Stage authority (post-lifecycle-migration):** `notification_stage` /
> `stage_changed_at` are NO LONGER stored in `engagement.json` — stage lives in
> the lifecycle DB (computed from `last_interaction_at`). The cap gate is what
> enforces the "stop after 2" guarantee; it does not rely on writing a Silent
> stage. `check-stage.py` is deprecated (still present but not in the live path);
> `mark-onboarding-done.py` still seeds `stage_changed_at` defensively for any
> legacy reader, but the activation flow does not depend on it. See the
> cross-system flag in § Activation nudge below.

---

### Activation nudge (greeted but never replied)

**Purpose:** Break the ice for users who came in via TDEE handoff, got the
welcome message, but have **never replied at all**. Different cohort from the
first-meal nudge (who replied + onboarded but never logged).

**Cron created by openclaw-infra, NOT this repo.** The infra side schedules two
one-shot crons at handoff time (T+24h `nudge=1`, T+72h `nudge=2`, user-local
daytime). This skill only implements what they fire into. Fixed payload contract:

```
First run: python3 {notification-composer:baseDir}/scripts/pre-send-check.py \
  --workspace-dir  --meal-type activation --tz-offset .
If output is NO_REPLY, stop and output NO_REPLY.
Otherwise run notification-composer for activation (nudge=1).
```

**Detection / defining gate** (`pre-send-check.py --meal-type activation`): the
target user is a handoff case — `health-profile.md` exists with
`Onboarding Completed: —` (NOT a date) and `channel-source.json > handoffAppliedAt`
set. The **defining cancel signal** is `channel-source.json > lastInboundAt`
(epoch ms, written by infra Phase-0 on every inbound): **if present at all, the
user has replied → NO_REPLY (cancel)**. Also NO_REPLY if `channel-source.json`
is missing/unreadable (**fail closed** — the target cohort always has the file;
if we can't confirm no-reply we stay silent), onboarding completed, any meal
logged, the authoritative lifecycle Silent stage (handled by the generic
`check_engagement_stage` gate via the lifecycle API), on leave/pause, or
`activation.nudges_sent >= 2`.

**Cap & terminal state:** Max **2** nudges (`activation.nudges_sent

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [NanoRhino](https://github.com/NanoRhino)
- **Source:** [NanoRhino/weight-loss-skill](https://github.com/NanoRhino/weight-loss-skill)
- **License:** MIT
- **Homepage:** https://nanorhino.com/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-nanorhino-weight-loss-skill-notification-manager
- Seller: https://agentstack.voostack.com/s/nanorhino
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
