*Social Media Manual — [index](../MANUAL.md)*

# 8. Schedules and calendar

A **schedule** is a rule that produces **occurrences** (calendar slots). Each slot either has
a post attached and publishes at its time, or is waiting for content. Schedules publish
without anyone present, which is why only an owner can create one unconditionally; an editor
needs a standing authorization on every account first (chapter 3).

## Creating a schedule

`social_create_schedule` with:

| argument | meaning |
|---|---|
| `account_ids[]` | destinations |
| `start` | first slot as `YYYY-MM-DD HH:MM` in the schedule's timezone |
| `timezone` | IANA name such as `America/Chicago`; default `UTC` |
| `frequency` | `once` (default), `daily`, `weekly`, `monthly` |
| `interval` | every N days / weeks / months, 1..52, default 1 |
| `weekdays[]` | weekly only: 1 = Monday … 7 = Sunday; default the start weekday |
| `until` | last slot, local `YYYY-MM-DD HH:MM` |
| `max_occurrences` | stop after N slots |
| content | exactly one of `post_id` (repeat one post), `series_post_ids[]` (one post per slot, in order), `brief` (what each slot should be about; slots start as `needs_content`) |

Reply: `schedule` with `schedule_id`, `status: active`, the rule as stored, `created_by`, and
`occurrences[]` materialized so far.

Time rules:

- slots are computed in the schedule's timezone, so a 09:00 slot stays 09:00 across daylight
  saving changes;
- monthly rules clamp to the last day of short months and return to the original day after;
- occurrences are materialized 30 days ahead and at most 60 per schedule; the minute tick
  extends the horizon as time passes;
- the schedule finishes (`status: finished`) when its rule is exhausted and no slot is open.

## Occurrence statuses

| status | meaning |
|---|---|
| `ready` | a post is attached; will publish at its time |
| `needs_content` | brief-only slot; an agent must attach a post with `social_fill_occurrence` |
| `publishing` | the tick handed it to the publishing service |
| `published`, `partial`, `failed`, `unknown` | the outcome of its publication (chapter 7); `publication_id` is set |
| `blocked` | the authorization it relied on is gone, or the post was archived; `note` says which |
| `missed` | still `needs_content` an hour after its time |
| `skipped` | skipped with `social_skip_occurrence` |
| `cancelled` | the schedule was cancelled or the authorization revoked |

## The calendar

`social_list_calendar` lists occurrences across every account the caller can see, with
`occurrence_id`, `schedule_id`, `account`s, `scheduled_at`, `status`, `post_id` / `post_title`,
`publication_id`, the schedule's `brief` and any `note`. Default window: yesterday to 30 days
ahead; `from` / `to` (ISO-8601) and `account_id` narrow it, `limit` up to 500.

Working a brief-driven schedule as an agent:

1. `social_list_calendar`, look for `needs_content` slots and read their `brief`.
2. Draft a post for the slot's accounts (chapter 6), preview it.
3. `social_fill_occurrence` with `occurrence_id` and `post_id`. The post must target the
   schedule's accounts. Filling a `ready` slot replaces its post.
4. The slot is now `ready` and publishes at its time under the schedule's authorization.

Do this well before the slot: an unfilled slot is `missed` an hour after its time and is not
retried.

## Changing a schedule

`social_update_schedule` with `action`:

| action | effect |
|---|---|
| `pause` | upcoming slots are kept; the tick ignores the schedule until `resume` |
| `resume` | picks up again on the next tick: `ready` slots whose time passed while paused are published late, `needs_content` slots more than an hour past are marked `missed` |
| `cancel` | every upcoming occurrence is cancelled; the schedule is `cancelled` for good (`SCHEDULE_CANCELLED` afterwards) |
| `brief` | replace the brief text (`brief` argument) |

`social_skip_occurrence` drops one slot and leaves the rest alone. Filling works on
`needs_content`, `ready`, `blocked` and future `missed` slots; skipping on `needs_content`,
`ready` and `blocked`. Anything else, or a slot whose time has passed, is `OCCURRENCE_LOCKED`.

## What runs the clock

A server cron runs every minute: it extends horizons, marks misses, and hands each due `ready`
slot to the publishing service after re-checking that every authorization the schedule relies
on is still active. The resulting publication looks exactly like one from `social_publish_now`
(chapter 7) and carries `schedule_id` and `occurrence_id` in its evidence. Delivery then
follows the normal queue, so a 09:00 slot is typically live within a minute or two of 09:00.
