vellum-ai

notifications

Send notifications through the unified notification router

vellum-ai 1,449 216 Updated 1mo ago
GitHub

Install

npx skillscat add vellum-ai/vellum-assistant/notifications

Install via the SkillsCat registry.

SKILL.md

Call this when something happened that the user would want to know about — a completed task with a notable outcome, an interesting observation, a positive trend you noticed in monitored data, useful research worth surfacing, a workflow that got blocked, a credential or token failure, etc. Do not call it for routine task completions where nothing notable happened. When in doubt and you have a real observation to share, share it.

Exception: you are running a schedule. The "was this notable?" test does not apply to a scheduled run. The user picked the cadence; the run happening at all is what they asked to see. A scheduled run that produces any user-facing output (a briefing, a digest, a report, or a check whose answer is "nothing changed") ends by sending that output as a notification. Writing it into the conversation and stopping does not reach the user: nobody is looking at a scheduled run's conversation.

That does not license noise. Judgment moves from whether to notify to what to say: a run with a genuinely empty result says so in one line rather than padding it, and a run that only did silent housekeeping (rotating a cache, syncing a file) with nothing to report stays quiet.

Watcher ticks are not scheduled runs. A watcher stays quiet unless its action prompt says this event is worth surfacing. Unmatched events and polls with nothing new must not produce a notification.

Sending Notifications

Always pass --title. Skipping it triggers a fallback that just truncates --message to 60 chars and shows it as the title — the user sees the same text twice with no scannability gained.

assistant notifications send \
  --title "Short headline" \
  --message "Your verbatim observation in your own words"

For time-sensitive items:

assistant notifications send --title "..." --message "..." --urgent

Command Reference

Flag Required Description
--message <message> Yes Notification body. Markdown (GFM) renders in the detail panel; the OS banner shows plain text.
--title <title> Yes in practice Short headline (≤ 8 words). Omitting it triggers a body-truncation fallback that shows up as a duplicate of --message — always write a real title.
--urgent No Mark as needing attention now/soon
--preferred-channels No Additive channel hints. Vellum stays selected.
--channels No Exclusive allowlist (e.g. telegram). Replaces the default set. Urgent delivery does not add vellum or platform. Wins over --preferred-channels.
--json No Output machine-readable JSON

Title

Write a --title for every notification. It's the only line the user sees in the lock-screen popup and the collapsed row of the notification list, so a short noun phrase (≤ 8 words) is what makes the notification scannable. If you omit --title, the system falls back to the first sentence of --message (truncated at 60 chars) — that's almost always worse than what you'd write, because it duplicates body text the user is already going to read.

Avoid restating the first sentence of --message verbatim — the title should add scannability, not duplicate.

Message

The body renders as markdown (GFM) in the home feed detail panel — where the user actually opens the notification on web, iOS, and macOS. Light markdown makes multi-fact bodies scannable. The OS lock-screen banner shows the body as plain text, so prefer inline emphasis over heavy structure that looks ugly unrendered.

Supported: **bold**, *italic*, `inline code`, fenced code blocks, links, bulleted and numbered lists, blockquotes, headings, GFM tables, ~~strikethrough~~.

Use it like this:

  • Bold the headline fact when the body has more than one sentence.
  • Bullets or numbered lists when surfacing multiple discrete items (failures, files touched, missed messages).
  • Inline code for identifiers, paths, commands, and short snippets.
  • Fenced code blocks for multi-line output (stack traces, diffs).

Avoid large headings (#, ##) and wide tables — they render fine in the panel but look noisy in the banner preview.

Urgent semantics

Use --urgent for items needing attention now/soon (blocked work, broken auth, time-sensitive issues). Skip for items the user should see when they have time.

Channel routing

--preferred-channels adds extra surfaces on top of the default set (vellum stays selected). --channels is exclusive: only those connected channels are selected. Use --channels telegram when the user asked for Telegram only. Home does not mirror an exclusive send unless vellum is in the list. When both flags are set, --channels wins.

Examples

# Plain notification — bold the headline fact
assistant notifications send \
  --title "Backup complete" \
  --message "Nightly backup finished — **12.4 GB** archived to cold storage across **3** datasets."

# Urgent notification — inline code for the identifier
assistant notifications send \
  --title "Auth token expired" \
  --message "Sync is paused until you reauthenticate the \`GitHub\` integration." \
  --urgent

Response Format

{
  "ok": true,
  "signalId": "...",
  "dispatched": true,
  "selectedChannels": ["telegram"],
  "deliveryResults": [],
  "receiptClass": "unknown"
}

dispatched means the pipeline attempted delivery. receiptClass is the strongest proof the adapters reported (provider_accepted, gateway_accepted, client_os_posted, or unknown). It is not proof the user saw a banner. Check selectedChannels and deliveryResults before telling the user the alert landed.

Reading Surfaced Notifications

assistant notifications list --json

Reads from the user's home feed ($VELLUM_WORKSPACE_DIR/data/home-feed.json) — the inbox that mirrors background and async notifications surfaced via the unified pipeline. Real-time chat pushes that did not mirror to the feed (direct Telegram/Slack/Vellum-chat sends without --is-async-background) will not appear here.

When to call

  • Before sending: check whether you already surfaced a similar item recently (filter by --conversation-id or --after to dedupe).
  • Catch-up summaries: when the user asks "what did I miss" or returns after a session break, list the items they haven't dismissed.
  • Lookup: when the user references a past notification ("the email thing you flagged earlier"), find it by --conversation-id or date range.

Filters

Flag Purpose
--all Include dismissed items (default: excluded — assistant cares about outstanding work)
--status <s> Filter by status (new / seen / acted_on / dismissed); repeatable. Overrides the --all default.
--before <iso> / --after <iso> ISO-8601 createdAt bounds (strict; = is excluded).
--urgency <u> Filter by urgency (low / medium / high / critical); repeatable.
--category <c> Filter by category (security / scheduling / background / email / system); repeatable.
--conversation-id <id> Only items tied to this conversation.
--from-assistant Only items the assistant herself emitted.
--noteworthy Only items flagged as noteworthy.
--limit <n> Default 20, max 200.
--offset <n> Pagination offset. Combine with --limit to walk older pages.

Examples

# What's outstanding right now (defaults: skip dismissed, newest first)
assistant notifications list --json

# Everything you've shown the user today
assistant notifications list --after 2026-05-28T00:00:00Z --all --json

# Only high-stakes items
assistant notifications list --urgency high --urgency critical --json

# Pre-send dedupe: anything you already surfaced for this conversation
assistant notifications list --conversation-id 7fab234c --after 2026-05-28T00:00:00Z --json

# Walk older pages
assistant notifications list --limit 20 --offset 20 --json

Response shape

{
  "ok": true,
  "items": [
    /* FeedItem records: id, title?, summary, status, urgency?, category?, conversationId?, createdAt, ... */
  ],
  "total": 12,
  "returned": 3,
  "hasMore": true,
  "updatedAt": "2026-05-28T10:30:00.000Z"
}

Editing Notifications

Use edit when an already-sent notification needs revising — a typo in the body, a status update on something you previously surfaced (e.g. "in progress" → "done"), or de-escalating the urgency of a stale alert. Prefer editing over re-sending: a fresh notification with the corrected text creates duplicate noise in the user's inbox and pings them twice.

assistant notifications edit --id <notif:uuid> --message "Corrected body"

Finding the id

The id field is the full notif:<uuid> printed by notifications list --json under items[].id. Bare uuids (without the notif: prefix) are also accepted.

assistant notifications list --json | jq '.items[] | {id, title, summary}'

Command Reference

Flag Required Description
--id <id> Yes Feed item id (notif:<uuid>) or bare uuid
--message <text> No* New body — updates the home-feed summary AND the delivered channel message where supported
--title <text> No* New short headline (≤ 8 words)
--urgency <level> No* Change urgency (low/medium/high/critical). Feed-only — does not re-push channel messages
--status <state> No* Lifecycle transition (new/seen/acted_on/dismissed). Feed-only
--json No Machine-readable JSON

*At least one of --message, --title, --urgency, or --status must be supplied.

Channel behavior

Channel Edit behavior
Home feed (macOS/iOS inbox) Always updated when the item exists.
Slack Updated in-place via chat.update when the original delivery captured a Slack ts. Deliveries older than this feature returned messageId: null and report outcome: "unsupported".
Push, email, SMS Cannot be edited — reported as outcome: "unsupported" in the result.

Response shape

{
  "ok": true,
  "feedItem": {
    "id": "notif:...",
    "title": "...",
    "summary": "...",
    "status": "new",
    "urgency": "low"
  },
  "channels": [
    { "channel": "slack", "deliveryId": "...", "outcome": "updated" },
    {
      "channel": "platform",
      "deliveryId": "...",
      "outcome": "unsupported",
      "reason": "platform adapter does not support in-place edits"
    }
  ]
}

outcome values: "updated" (channel message edited successfully), "unsupported" (channel cannot edit at all), "skipped" (delivery wasn't in sent status), "failed" (channel-side error — see reason).

Examples

# Fix a typo in the body
assistant notifications edit \
  --id notif:abc12345-... \
  --message "Backup completed — 12.4 GB archived to cold storage."

# De-escalate an urgent alert that resolved itself
assistant notifications edit --id notif:abc12345-... --urgency low

# Dismiss a notification you previously surfaced
assistant notifications edit --id notif:abc12345-... --status dismissed

Important

  • Do NOT use AppleScript display notification or other OS-level notification commands for assistant-managed alerts. Always use assistant notifications send.
  • For a digest, summary, or report that should land in a specific chat or email destination, use messaging_send. It reaches Gmail and Outlook as a draft, and posts to a Slack, Telegram, Discord, or WhatsApp chat through that channel's own transport, where the post is recorded.
  • For the user's notification inbox and connected push channels, use assistant notifications send and pass the complete authored body as --message. The pipeline keeps that body. Do not rewrite it into a short alert first. A scheduled run should also pass --source-channel scheduler.
  • Send notifications that fire immediately with no delay capability. For one-time future alerts, use schedule_create with fire_at. For recurring alerts, use schedule_create with an expression (cron/RRULE).