> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blobhub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Scheduler overview

> What a scheduler deployment is, the two blobs one deploy touches, and the four commands that manage it

A [scheduler blob](/blob-types/scheduler/overview)'s revision holds schedules, and each schedule fires a workflow
on a cron. Created in the browser, that set is a collection of forms; the `scheduler` command group makes it a
file — one manifest that lists every schedule the revision should carry, with its target written in names rather
than in UUIDs.

The manifest is the unit of work, exactly as it is for the `workflow` group. It names a blob and a revision, lists
every schedule that belongs on that revision, and every command in this group starts by loading it.

## The shape of a deployable project

One file. A scheduler blob holds no code and no documents, so there is nothing for the manifest to point at:

```
my-ops/
└── schedules.yaml              # the revision, and every schedule that belongs on it
```

`schedules.yaml`, for a revision holding one schedule that runs the `checkout_flow` workflow every morning:

```yaml theme={null}
type: scheduler_blob_deployment
version: "1.0"
blob: acme-corp/checkout-ops             # the scheduler blob — <org>/<blob>, or a bare <blob> plus --org
revision: latest                         # a scheduler blob is `managed`: one revision, always
schedules:
  - alias: daily_checkout_report
    repeat: recurring_cron
    cron_expression: "0 9 * * ? *"
    timezone: America/New_York
    enabled: true
    target:
      blob: checkout                     # a bare target blob inherits the manifest's own org
      revision: latest
      session: scheduled_runs
      workflow: checkout_flow
      input_data: []
```

The target is written entirely in symbols a person can read. What the platform stores in their place is UUIDs — a
revision id, a session id. Turning one into the other, in both directions, is most of what this group does. The
full grammar is in the [Scheduler manifest](/cli/scheduler/manifest) reference.

## A schedule targets a workflow in another blob

The schedule lives in the scheduler blob. What it runs does not — `target:` names a *workflow* blob, its revision,
a session on that revision, and a workflow definition by alias. So a single `scheduler deploy` legitimately spans
both of the platform's engines: it writes schedules with `scheduler_blobhub` against its own blob, while
validating every target with `workflow_blobhub` calls against the target revision.

```mermaid theme={null}
flowchart LR
    subgraph repo["Your repository"]
        MAN["schedules.yaml<br/>one entry per schedule"]
    end

    subgraph sched["Scheduler blob — scheduler_blobhub"]
        REC["Schedule records<br/>cron, timezone, resolved target"]
    end

    subgraph tgt["Target workflow blob — workflow_blobhub"]
        direction TB
        DEF["Workflow definition<br/>checkout_flow"]
        SES["Session<br/>scheduled_runs"]
    end

    MAN -->|"deploy: create or update, by alias"| REC
    MAN -.->|"deploy: validate the target,<br/>find or create the session"| DEF
    REC ==>|"every fire: one execution"| SES
    REC -.->|pull| MAN
```

**A key that can write the scheduler blob is not sufficient.** Firing a schedule writes an execution into the
target revision, so the platform requires **WRITE** on the target blob as well — the same role it gates the
scheduler blob itself on. Read access on the target is not enough, and a key that has only that will still be
refused.

The target revision itself is only ever read by the CLI, though. Deploying a schedule creates a session on that
revision but never touches the revision's own content, so scheduling against a target in `commit` phase is fine —
which is the normal case, since what you schedule is usually a committed workflow.

Two more consequences are worth knowing before the first deploy:

* **The platform reports every target failure as the same opaque `403`.** A missing target revision, a target you
  cannot reach, an unresolvable session, a session belonging to a different revision, and a workflow alias that
  resolves to nothing are deliberately indistinguishable, so that a caller cannot tell "does not exist" from
  "exists but you may not see it". That is a permanent security property. It is also why `deploy` probes the
  target itself, one step at a time, before writing anything: the CLI can name which of the five was wrong, and
  the platform structurally cannot.
* **`workflow:` is re-resolved every time the schedule fires.** The alias is stored as a string and sent verbatim,
  so the definition behind it can be replaced freely — but renaming or deleting it leaves the schedule pointing at
  a name that resolves to nothing, invisibly, until the next fire. `deploy` and `diff` check that the alias is a
  live `workflow`-category definition on the target revision for exactly this reason.

## `pull` and `deploy` are not a symmetric pair

In the `workflow` group, `pull` and `push` are a symmetric pair: a document that goes out through one and comes
back through the other comes back byte-symmetric. Nothing here works like that.

| Direction | What it really does                                                                                                                                                                           |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `pull`    | Reverses each stored schedule back into symbols — a revision id to `latest` while it still leads, a session id to its alias, a target blob to a bare name when its org is the manifest's own. |
| `deploy`  | Resolves those symbols into UUIDs, and **may create a `session:` alias that did not exist**.                                                                                                  |

That last side effect is the asymmetry. `pull` cannot undo it: nothing in a manifest deletes a session, and the
CLI will not delete runtime state it cannot prove it created. So a round trip through these two is not the
identity, and treating it as one is how you end up with sessions you did not intend.

**The group has no `push`, and that is deliberate rather than unfinished.** `push` means one specific thing in
this CLI — a code-unaware, byte-symmetric transport that interprets nothing. The scheduler's local-to-remote
operation resolves references, creates sessions, and validates four things about every target. Calling that
`push` would make one verb mean "move bytes" in one group and "resolve, create, validate" in another. It is
`deploy`, which already means *the composite that makes the manifest real and checks its own work*.

## The four commands

```bash theme={null}
blobhub scheduler ls     -f schedules.yaml
blobhub scheduler pull   -f schedules.yaml
blobhub scheduler diff   -f schedules.yaml
blobhub scheduler deploy -f schedules.yaml [--dry-run]
```

| Command  | What it does                                                                                                                | Network | Writes                           |
| -------- | --------------------------------------------------------------------------------------------------------------------------- | ------- | -------------------------------- |
| `ls`     | Lists the revision's schedules — alias, id, repeat, enabled, state and target workflow — marking which the manifest covers. | Read    | Nothing                          |
| `pull`   | Reverses every deployed schedule into symbols and **overwrites the manifest file** with the result.                         | Read    | The manifest file                |
| `diff`   | Reports, per alias, whether an entry would be created, updated (naming the fields that differ) or is unchanged, plus drift. | Read    | Nothing                          |
| `deploy` | Creates what is absent and updates what differs, by alias, after validating every target.                                   | Write   | Schedules, and possibly sessions |

`deploy` is the one to reach for day to day, and `diff` is what to run before it. `ls` and `diff` are the two
that write nothing at all, locally or remotely.

## Drift is not inert here

Schedules on the revision that the manifest does not cover are **drift**. As everywhere else in this CLI, drift is
reported, left untouched, and still exits `0` — but the consequence is different in this group, and the messages
say so.

An orphaned definition sits there doing nothing until something deploys or executes it. An orphaned schedule
**keeps firing on its cron**: it consumes execution quota, appends to its session on every fire, and can saturate
that session on its own. The message says so, names `blobhub scheduler prune` as the removal path, and states
that it is not yet available — so removing one today means the web UI, or a `delete_schedule` call.

One kind of drift can never be resolved from a manifest at all. A schedule's `alias` is **optional platform-side**
— the web UI's create form offers a blank field — and nothing declarative can address a schedule that has none.
`ls` shows it anyway, by id, because a schedule you cannot see is worse than one you cannot manage; `pull` skips
it and reports it separately, since writing an aliasless entry would produce a manifest this CLI's own loader
rejects.

## What every command shares

**The manifest is always explicit.** `-f/--file` is required on all four commands; there is no default path and no
search of the working directory.

**Every command operates on the whole schedule set.** There is no per-schedule narrowing flag — no counterpart to
the `workflow` group's `--definition`. A manifest describes one revision's schedules, and these commands act on
that set.

**`--dry-run` belongs to `deploy` alone, and it is not free of side effects.** It skips the
`create_schedule`/`update_schedule` calls, and nothing else: every target is still resolved, which means a
`session:` alias that does not exist yet **is still created**. `diff` is the command that does not — it reports an
absent session as one `deploy` would create. That is the one place the two genuinely differ in cost.

**Global flags come before the group**, since they belong to the binary rather than the command:

```bash theme={null}
# --json and --org are global; -f belongs to the subcommand
blobhub --json --org acme-corp scheduler diff -f schedules.yaml
```

**Only `deploy` needs a writable revision.** It requires status `ready` and phase `draft` or `managed`; `ls`,
`pull` and `diff` also accept `commit`. A scheduler blob is created in phase `managed` with a single revision, so
in practice `revision: latest` names the one revision there is and the gate is rarely what stops you.

## See also

* [Scheduler manifest](/cli/scheduler/manifest) — the `scheduler_blob_deployment` body in full, and the four
  constraints checked before anything is written.
* [Concepts](/cli/concepts) — manifests, drift, and the platform behaviours behind several messages in this group.
* [Configuration](/cli/configuration) — credentials, the precedence ladder, output streams and the `--json`
  envelope.
* [Error codes](/cli/error-codes) — every code these commands can raise, with its remediation.
* [Scheduler blob type](/blob-types/scheduler/overview) — cron syntax, timezones, date windowing, schedule states
  and what the platform records for every fire.
* [Workflow overview](/cli/workflow/overview) — the group this one is modelled on, where `pull` and `push` really
  are a symmetric pair.
