# Scheduling (/docs/social/scheduling)



Find the schedule view at
`/project/{projectId}/dashboard/scheduled-posts` (also reachable from the
dashboard's Scheduled posts widget).

## Automatic mode [#automatic-mode]

Default. The `social-distribution-plan` workflow runs:

* On config change — `refresh-schedule` fires automatically when any of
  `distributionMode`, `willPublish`, `postsPerDay`, `schedule.times`,
  `contentLayerSourceIds`, or `generationMode` change.
* Nightly — `refresh-schedule-scheduled` runs on cron `0 3 * * *` UTC.

Each run:

1. Loads the project's timezone.
2. Validates the connected social account.
3. Loads the latest health snapshot (if any) to pick an account
   "temperature" — `cold` (1 slot/day at 10:00), `warm` (2 slots at
   10:00 + 15:00), `hot` (3 slots at 10:00 + 15:00 + 19:00).
4. Computes slots for a dynamic horizon — `max(3 days,
   ceil(availableContent / slotsPerDay))`.
5. Upserts `scheduled_posts` rows for each slot.
6. Attaches ready content\_containers to the earliest empty slots.
7. Optionally requests content generation to fill remaining gaps when
   `generationMode = "automatic"`.

## Custom schedule [#custom-schedule]

Set `distributionMode: "custom"` and supply times as `HH:MM` in the
project timezone:

```json
{
  "distributionMode": "custom",
  "schedule": {
    "times": ["09:00", "13:00", "18:00"]
  }
}
```

Only 24-hour `HH:MM` entries are supported (no day-of-week syntax).
`postsPerDay` equals `schedule.times.length`; allowed range is 1–5.

## Manual mode [#manual-mode]

`distributionMode: "manual"` — the planner creates zero slots. Use this
if you want to hand-pick every schedule in the UI.

## Time zones [#time-zones]

The project timezone determines slot computation. The planner converts
local `HH:MM` values to UTC before writing `scheduled_posts.scheduled_at`.
Changing the project timezone later does **not** shift already-scheduled
posts.

## Refresh-schedule command [#refresh-schedule-command]

`refresh-schedule` runs `social-distribution-plan` immediately. Invoke
it from the layer's **Commands** panel after changing config if you
don't want to wait for the onConfigChange effect.

## Recover overdue [#recover-overdue]

`recover-overdue` runs every 5 minutes and re-slots any `ready` or
`failed` post whose `scheduled_at` has passed. YouTube posts (dormant
platform) are explicitly skipped.
