schedule

$npx mdskill add vellum-ai/vellum-assistant/schedule

Manage scheduled automations. Schedules can be **recurring** (cron or RRULE expression) or **one-shot** (a single `fire_at` timestamp). Schedules support three modes: **execute** (run a message through the assistant), **notify** (send a notification to the user), and **script** (run a shell command directly without LLM involvement).

SKILL.md
.github/skills/scheduleView on GitHub ↗
---
name: schedule
description: Recurring and one-shot scheduling - cron, RRULE, or single fire-at time
compatibility: "Designed for Vellum personal assistants"
metadata:
  emoji: "📅"
  vellum:
    display-name: "Schedule"
    activation-hints:
      - "User wants to set a reminder for a future time (e.g. 'remind me at 9am tomorrow', 'remind me to take meds at 8pm')"
      - "User wants to schedule a recurring task or automation (e.g. 'every weekday at 9am', 'every Monday at noon')"
      - "User wants to schedule a one-time future action the assistant should run autonomously (e.g. 'at 5pm check my email and summarize it')"
      - "User wants to list, update, cancel, or inspect existing scheduled tasks or reminders"
    avoid-when:
      - "User wants to add something to their task list or queue — use task-list tools instead"
      - "User wants to act immediately or run a quick command that completes within the conversation — schedule is only for deferred or recurring execution"
---

Manage scheduled automations. Schedules can be **recurring** (cron or RRULE expression) or **one-shot** (a single `fire_at` timestamp). Schedules support three modes: **execute** (run a message through the assistant), **notify** (send a notification to the user), and **script** (run a shell command directly without LLM involvement).

## Schedule Syntax

### Cron

Standard 5-field cron syntax: `minute hour day-of-month month day-of-week`

| Field        | Values        | Special characters |
| ------------ | ------------- | ------------------ |
| Minute       | 0-59          | , - \* /           |
| Hour         | 0-23          | , - \* /           |
| Day of month | 1-31          | , - \* /           |
| Month        | 1-12          | , - \* /           |
| Day of week  | 0-7 (0,7=Sun) | , - \* /           |

Examples:

- `0 9 * * 1-5` - weekdays at 9:00 AM
- `30 8 * * *` - every day at 8:30 AM
- `0 */2 * * *` - every 2 hours
- `0 9 1 * *` - first of every month at 9:00 AM

### RRULE (RFC 5545)

iCalendar recurrence rules for complex patterns. Must include a DTSTART line.

Supported lines (all expressions must include DTSTART + at least one RRULE or RDATE):

| Line      | Purpose                                                 |
| --------- | ------------------------------------------------------- |
| `DTSTART` | Start date/time anchor (required)                       |
| `RRULE:`  | Recurrence rule (multiple lines = union of occurrences) |
| `RDATE`   | Add one-off dates not covered by the pattern            |
| `EXDATE`  | Exclude specific dates from the set                     |
| `EXRULE`  | Exclude an entire recurring series                      |

Exclusions (EXDATE, EXRULE) always take precedence over inclusions (RRULE, RDATE).

#### Basic examples

- `DTSTART:20250101T090000Z\nRRULE:FREQ=DAILY` - every day at 9:00 AM UTC
- `DTSTART:20250101T090000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR` - Mon/Wed/Fri at 9:00 AM UTC
- `DTSTART:20250101T090000Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1,15` - 1st and 15th of each month

#### Bounded recurrence

- `DTSTART:20250101T090000Z\nRRULE:FREQ=DAILY;COUNT=30` - daily for 30 occurrences then stop
- `DTSTART:20250101T090000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20250331T235959Z` - every Monday until end of March

#### Set construct examples

- `DTSTART:20250101T090000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR\nEXDATE:20250120T090000Z` - Mon/Wed/Fri except Jan 20
- `DTSTART:20250101T090000Z\nRRULE:FREQ=DAILY\nEXRULE:FREQ=WEEKLY;BYDAY=SA,SU` - every weekday (daily minus weekends)
- `DTSTART:20250101T090000Z\nRRULE:FREQ=MONTHLY;BYMONTHDAY=1\nRDATE:20250704T090000Z` - 1st of each month plus July 4th
- `DTSTART:20250101T090000Z\nRRULE:FREQ=WEEKLY;BYDAY=TU\nRRULE:FREQ=WEEKLY;BYDAY=TH` - union of Tuesdays and Thursdays

## One-Shot Schedules (Reminders)

To create a one-time schedule that fires once and is done, pass `fire_at` (an ISO 8601 timestamp) instead of an `expression`. This replaces the old reminder concept - "remind me at 3pm" becomes a one-shot schedule with `fire_at`.

One-shot schedules:

- Fire once at the specified time, then are marked as `fired` and disabled.
- Support both `execute` and `notify` modes (see below).
- Can be cancelled before they fire.

Examples:

- "remind me at 3pm" → `schedule_create` with `fire_at: "2025-03-15T15:00:00-05:00"`, `mode: "notify"`
- "at 5pm, check my email and summarize it" → `schedule_create` with `fire_at`, `mode: "execute"`

## Mode

The `mode` parameter controls what happens when a schedule fires:

- **execute** (default) - sends the schedule's message to a background assistant conversation for autonomous handling. The assistant processes the message as if the user sent it.
- **notify** - sends a notification to the user via the notification pipeline. No assistant processing occurs.
- **script** - runs the `script` field as a shell command directly. No LLM invoked, no conversation created. stdout/stderr are captured in the schedule run record. Exit code 0 = success, non-zero = error. Commands run in the workspace directory with a 60-second timeout by default. Override the timeout per schedule with `timeout_ms` (range 1000–1800000 ms) when a script needs more or less time; pass `timeout_ms: null` on update to revert to the default. The guardian can also adjust this from the /assistant/settings/schedules page.

Use `notify` for simple reminders ("remind me to take medicine at 9am"), `execute` for tasks that need assistant action ("check my calendar at 8am and send me a digest"), and `script` for lightweight shell automations that don't need LLM involvement ("refresh a cache", "poll an API", "rotate logs").

## Conversation Reuse

Recurring schedules reuse the same conversation across runs by default — subsequent runs continue the conversation from the last successful run, preserving context and channel thread continuity. Set `reuse_conversation: false` explicitly if each run should start with a fresh conversation (e.g. independent reports that shouldn't accumulate prior context). One-shot schedules always create a fresh conversation.

- Only applies to **recurring** schedules; ignored for one-shot schedules.
- If the prior conversation has been deleted, a new one is created automatically.
- On the first run (no prior conversation), a new conversation is created as usual.

## Routing (notify mode)

Control how notify-mode schedules are delivered at trigger time with `routing_intent`:

- **single_channel** - deliver to one best channel
- **multi_channel** - deliver to a subset of channels
- **all_channels** (default) - deliver to every available channel

Optionally pass `routing_hints` (a JSON object) to influence routing decisions (e.g. preferred channels, exclusions).

### Routing Defaults

- **Default to `all_channels`** for most notifications. Users usually want to be notified wherever they are.
- **Use `single_channel`** only when the user explicitly specifies a single channel (e.g. "remind me on Telegram").
- **Determine the originating channel** for routing hints using this priority:
  1. **`source_channel`** from `<turn_context>` — use directly if present. This is the authoritative channel name.
  2. **`interface` fallback** — if `source_channel` is absent (common for guardian/direct users), map the `interface` value to a channel name:
     | `interface` value | Channel name |
     | --- | --- |
     | `macos`, `ios` | `vellum` |
     | `telegram` | `telegram` |
     | `slack` | `slack` |
     | `cli` | _(omit — no routable channel)_ |
  3. If neither field is present or the interface is `cli`, omit `preferred_channels`.

  When a channel is determined, include it as a routing hint:

  ```
  routing_hints: { preferred_channels: ["<resolved channel>"] }
  routing_intent: "all_channels"
  ```

## Tool Input

Use `syntax` + `expression` to specify the schedule type explicitly, or just `expression` to auto-detect. For one-shot schedules, use `fire_at` instead of `expression`.

## Lifecycle

1. Create a schedule with a name and either an expression (recurring) or fire_at (one-shot), plus a message.
2. At each trigger time, the message is dispatched to the assistant (execute mode) or a notification is sent (notify mode).
3. Schedules can be enabled/disabled, updated, or deleted. One-shot schedules are automatically disabled after firing.

## Tips

- **When the user specifies a name for the schedule, use it exactly as given.** Do not paraphrase, embellish, or generate a descriptive name.
- Use `schedule_create` for both recurring automation ("every day at 9am") and one-time reminders ("remind me at 3pm").
- For task tracking ("add to my tasks", "add to my queue"), use task_list_add instead.
- `fire_at` must be a strict ISO 8601 timestamp with timezone offset or Z (e.g. `2025-03-15T09:00:00-05:00`).

### Anchored & Ambiguous Relative Time

Phrases like "at the 45 minute mark", "at the top of the hour", "at noon", or "20 minutes in" are **clock-position or anchored relative time** expressions. Do NOT treat them as offsets from now.

**Resolution rules (in priority order):**

1. **Conversation-anchored expressions** - if the user mentioned a start time earlier in conversation ("I got here at 9", "meeting started at 2:10"), interpret offset-style phrases ("the 45 minute mark", "20 minutes in") as `start_time + offset`.

2. **Clock-position expressions** - when no start time is in context, map directly to a wall-clock time:
   - "top of the hour" → next :00
   - "the X minute mark" → current hour's :XX; if already past, advance one hour
   - "noon" / "midnight" → 12:00 PM or 12:00 AM today; if past, tomorrow

3. **Ask only if truly ambiguous** - if neither rule resolves, ask for clarification. Never silently default to "from now."

- Timezones default to the system timezone if omitted. Use IANA timezone identifiers (e.g. "America/Los_Angeles").
- Prefer RRULE for complex patterns that cron cannot express (e.g. "every other Tuesday", "last weekday of the month").

## Capability Preflight

Before confirming a schedule to the user, you MUST verify that you have the capabilities needed to execute the scheduled message autonomously. Scheduled messages run without user interaction - if a required integration is missing, the schedule will fail silently.

When `schedule_create` returns, it includes an integration status summary. Cross-reference the scheduled task's requirements against the available integrations:

- If the task involves **email** (reading, sending, OTP verification): an email integration must be connected (check the "email" category)
- If the task involves **making calls**: Twilio must be connected
- If the task involves **web browsing or form-filling**: browser automation must be available (check client type)
- If the task involves a **multi-step workflow** (e.g., book appointment → read confirmation email), trace the full dependency chain

If any required capability is missing:

1. **Do NOT tell the user the schedule is ready** - instead, explain what's missing and why the schedule won't work yet
2. Offer to help set up the missing integration first
3. The schedule is still created (so timing is preserved), but make it clear it won't execute successfully until dependencies are resolved

## Delivering Results

Scheduled messages run without user interaction. If the task produces output that the user should see (e.g. a digest, summary, or report), the scheduled message **must** include an explicit instruction to deliver the results. Without this, the output only lives in the conversation log and never reaches the user.

Choose the right delivery tool based on the content:

- **Rich content** (digests, summaries, reports): For Gmail, use `messaging_send` with the target platform and conversation ID. For Slack, use the Slack Web API directly via CLI (`chat.postMessage`). This preserves the full content and posts directly.
- **Short alerts** (status updates, completion notices): Use `assistant notifications send` via `bash` to let the notification router pick the best channel. Note: the router's decision engine rewrites content into short alerts, so it is not suitable for rich content.

Example schedule message for a Slack digest:

> "Scan my Slack channels for the last 24 hours using the Slack Web API via bash (network_mode: proxied, credential_ids: ['slack_channel/bot_token']), then post the summary to #alex-agent-messages (C0A7STRJ4G5)."
More from vellum-ai/vellum-assistant