---
name: scheduling
description: "Calendar and scheduling — create events, manage appointments, set up recurring triggers."
tools:
  - name: schedule-event
    publicAllowlist: false
    adminAllowlist: false
  - name: schedule-list
    publicAllowlist: false
    adminAllowlist: false
  - name: schedule-get
    publicAllowlist: false
    adminAllowlist: false
  - name: schedule-update
    publicAllowlist: false
    adminAllowlist: false
  - name: schedule-cancel
    publicAllowlist: false
    adminAllowlist: false
  - name: schedule-export-ics
    publicAllowlist: false
    adminAllowlist: false
  - name: schedule-import-ics
    publicAllowlist: false
    adminAllowlist: false
  - name: schedule-archive-ics
    publicAllowlist: false
    adminAllowlist: false
  - name: time-resolve
    publicAllowlist: false
    adminAllowlist: true
skills:
  - skills/daily-prep/SKILL.md
  - skills/briefing/SKILL.md
always: false
embed: false
metadata: {"platform":{}}
mcp:
  command: node
  args:
    - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
    - ${PLATFORM_ROOT}/plugins/scheduling/mcp/dist/index.js
  env:
    MCP_SPAWN_TEE_NAME: scheduling
    LOG_DIR: ${LOG_DIR}
    PLATFORM_ROOT: ${PLATFORM_ROOT}
    ACCOUNT_ID: ${ACCOUNT_ID}
    SESSION_ID: ${SESSION_ID}
    AGENT_SLUG: ${AGENT_SLUG}
mcp-manifest: auto
---

# Scheduling

Manages events, appointments, and recurring triggers in the graph. Events are discoverable via memory-search.

## When to create events

Create events immediately when the user identifies something time-bound — an appointment, a reminder, a recurring briefing. Confirm after creation; don't ask permission first.

## One-time vs recurring

- **One-time events** have a `startDate` and optionally an `endDate`. They transition to `due` when their time arrives (via the platform cron) and to `completed` when the agent or user marks them done.
- **Recurring events** have a `recurrence` (cron expression) and a computed `nextRun`. They stay in `scheduled` status permanently. The cron advances `nextRun` and sets `lastTriggered` each cycle.

## Cron expressions

Use standard 5-field cron syntax: `minute hour day-of-month month day-of-week`.

| Pattern | Meaning |
|---------|---------|
| `0 8 * * 1-5` | Weekdays at 8am |
| `0 9 * * 1` | Every Monday at 9am |
| `0 0 1 * *` | First of each month at midnight |
| `*/30 * * * *` | Every 30 minutes |

## Skip next

For recurring events, `schedule-update` with `skipNext: true` advances `nextRun` by one cycle without triggering. Use when the user says "skip tomorrow's briefing" or similar.

## Cancellation

`schedule-cancel` sets status to `cancelled`. For recurring events this kills the entire series. There is no per-occurrence cancellation — use skip-next for that.

## Event actions

Events can carry an automated action that fires when the event's time arrives. The dispatcher binary that reads due events and spawns the target plugin's MCP server lives at `scheduling/mcp/dist/scripts/check-due-events.js`. It is **not currently scheduled on any install**: the legacy crontab writer was removed and migration of the dispatcher to Desktop scheduled tasks (the canonical dispatch surface — see `maxy-code-prd.md` §Scheduled tasks) is tracked separately. Until that landing, `action:` payloads are stored on the event and only execute when an operator invokes the dispatcher manually.

To create an event with an action, pass the `action` parameter to `schedule-event`:

```json
{ "plugin": "memory", "tool": "memory-search", "args": { "query": "open commitments" } }
```

- `plugin` — the plugin directory name under `$PLATFORM_ROOT/plugins/` (validated at creation — must exist)
- `tool` — the MCP tool name to call (validated at dispatch — if it doesn't exist, the error is logged)
- `args` — optional arguments passed to the tool call

Actions are stored as flat properties on the Event node: `actionPlugin`, `actionTool`, `actionArgs` (JSON string). Use `schedule-update` with `action` to modify or `action: null` to clear.

Dispatch results are recorded on the Event node (`lastDispatchResult`, `lastDispatchAt`) and appended to the notes history. The admin can also read dispatch logs via `logs-read` with `type: "heartbeat"`.

Users and plugins both create events with actions via the same `schedule-event` interface — the mechanism is symmetric.

## Ingesting calendar archives (.ics → graph)

`schedule-archive-ics` is the bulk-ingest path: it reads an `.ics` export from any calendar app (Google, Apple, Outlook, Fastmail, etc.) and writes one `:Meeting` per VEVENT into the graph, with attendees resolved to `:Person` by lowercased email. Distinct from `schedule-import-ics`, which only parses + returns events for the operator to recreate via `schedule-event` one at a time.

Two-mode flow:

- `mode: 'dry-run'` — parses the file, classifies every attendee against the existing graph, returns counts plus up to 20 sample resolutions (already-existing under this account, existing under a foreign account that will be skipped, new stub to be created, or non-mailto resource). No writes.
- `mode: 'commit'` — applies the import. Each meeting MERGEs on `(accountId, uid)` (idempotent re-ingest), anchors to the calling `:AdminUser` via `[:IMPORTED]`, and writes `(:Person)-[:ATTENDED]->(:Meeting)` edges with role + partstat.

Cross-account collision rule: when an attendee email already exists as a `:Person` under a different `accountId`, the import skips the attendance edge (no edge written, the foreign Person is untouched) and counts it as `skippedCrossAccountPerson`. Absolute brand isolation is preserved — one operator's import cannot graft attendance onto another operator's people.

Description bodies pass through unchanged on `:Meeting.description`. The typed-edge auto-extraction pass already iterates `:Meeting` and the `Meeting -[:MENTIONS]-> Person/Organization` plus `Person -[:ATTENDED]-> Meeting` schema, so commitments + mentions surface without extra extraction code in this plugin.

Out of scope on this surface: live Google/Outlook/Apple API sync, OAuth, incremental cursor, `:Day` aggregator, `:RecurringMeeting` parent, attendee auto-enrichment, write-back to the source calendar.

## Locale time formatting

All timestamps in tool responses (schedule-list, schedule-get, schedule-event) are formatted in the user's locale timezone with a relative delta. The user's timezone is read from `UserProfile.timezone` (IANA format, e.g. `Europe/London`).

Example output: `Thursday 3 April 2026, 12:00 BST (in 51 minutes)` instead of raw `2026-04-03T11:00:00.000Z`.

Relative deltas follow standard cutoffs: < 1 min ("just now"), minutes, hours, days (1–6), and no relative for 7+ days. Storage remains UTC — formatting is applied at output time only.

The platform auto-detects the server's IANA timezone and sets it on `UserProfile.timezone` at first login (and backfills existing profiles that lack one). Onboarding step 7 confirms the auto-detected timezone with the user. If `UserProfile.timezone` is still not set (e.g. Neo4j was unreachable during the backfill), the tool returns an error instructing the agent to set the timezone — there is no fallback to UTC. To set the user's timezone manually, use `profile-update` (memory plugin) with `profileFields: { timezone: 'Europe/London' }`. The timezone must be a valid IANA identifier (e.g. `Europe/London`, `America/New_York`, `Asia/Tokyo`).

## Time resolution

Any activity involving time (date, timestamp, day of week, month, duration) routes through `time-resolve` before any other reasoning. Returns the user's locale timezone view of the input expression with a human-readable relative delta. Input format: `expression` is an ISO 8601 datetime string.

## Relationships

Link events to other graph entities using relationship types:

- `ABOUT` — what the event concerns (a Service, Task, etc.)
- `SCHEDULED_FOR` — who the event is for (a Person)
- `PART_OF` — which conversation created the event

The `sessionKey` parameter on `schedule-event` automatically creates the `PART_OF` relationship.

## ICS calendar files

Export events as `.ics` files so users can add them to Apple Calendar, Google Calendar, or Outlook. Use `schedule-export-ics` with the event IDs; it writes the `.ics` under the account's `output/ics/` directory and returns the path. Deliver that file to the operator (see the `file-presentation` skill for how delivery works on this surface).

Import `.ics` files uploaded by the user. Use `schedule-import-ics` with the attachment path. Present the extracted events for confirmation before creating them via `schedule-event`.

Cron→RRULE mapping for recurring events: daily, weekly-with-days, and monthly-by-date patterns produce standard RRULE properties. Cron patterns that cannot map to RRULE (e.g. `0 8 1,15 * *`) produce individual VEVENTs per occurrence instead.

## References
- plugins/business-assistant/references/scheduling.md (booking protocol, geographic clustering, briefings)
