# Scheduling & Workflows (/docs/concepts/scheduler)



Layers runs time-based work on two tiers:

1. **Per-layer schedules** — every project layer has a cadence for its
   scheduled commands (defaulting to the layer template, overridable per
   project). Due commands fire on their own cadence.
2. **Global schedules** — a set of platform-wide schedules that handle
   cross-project work (platform sync, ads metrics, optimization, scoring,
   reconciliation).

You don't normally interact with the scheduler directly — Layers takes care
of it. But understanding the cadence is useful for debugging timing.

## Per-layer cron overrides [#per-layer-cron-overrides]

Each layer template defines a default cron for its `*-scheduled` commands.
You can override per-project by setting `project_layers.schedule.cron`:

```json
{
  "schedule": { "cron": "37 9 * * *" }
}
```

This is useful for:

* Timezone-specific batch windows ("run at 9:37 UTC for the EU morning").
* Smoothing load (if many projects share a default cron, override the busy
  ones to spread).
* Per-client SLAs (premium clients get faster cadence).

The scheduler prefers the per-layer override over the template default,
so there's exactly one cron per layer at any moment. No double execution.

## Global cadence [#global-cadence]

Representative global cadences you can count on:

| Domain                                     | Cadence      |
| ------------------------------------------ | ------------ |
| Platform quick sync (counts, recent posts) | every 5 min  |
| Platform full sync                         | every 20 min |
| Platform posts sync                        | every 30 min |
| Platform metadata sync                     | every 6h     |
| Ads metrics sync (Meta / TikTok / Apple)   | every 6h     |
| Ads status sync (Meta / TikTok / Apple)    | daily        |
| Ads optimizer                              | daily        |
| Organic performance scoring                | every 4h     |
| Orphan campaign detector                   | daily        |
| Ads stuck-sync reaper                      | every 6h     |
| RevenueCat metrics                         | hourly       |
| Stripe metrics                             | hourly       |
| Trending music refresh                     | every 12h    |
| MSD reconcile                              | every 15 min |
| Orphaned container cleanup                 | every 4h     |
| Webhook dispatcher tick                    | every 1 min  |

Schedules are non-overlapping — if the previous run hasn't finished, the
next trigger is skipped rather than queued.

## Workflows [#workflows]

Every layer command runs as a durable workflow. Workflows:

* Are durable — they survive restarts.
* Are versioned — each workflow has a slug + version.
* Are idempotent at the step level — individual steps can be retried freely.

You can introspect workflow runs in the **Workflow runs** panel of any layer
settings page.

## Failure handling [#failure-handling]

When a workflow fails:

1. The failure is recorded with a full trace.
2. The failure is published to the notifications system.
3. The workflow can be retried from the UI.
4. Content-generation workflows that fail mid-flight mark their target
   `content_container` as `failed` (so they don't appear stuck in `processing`).

See [Failure modes](/docs/paid-media/failure-modes) for SLAs on each integration.
