> ## 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.

# blobhub scheduler diff

> Report per-alias create/update/unchanged plus drift, without writing or deploying anything

`diff` compares the manifest against the schedules the revision actually carries and reports, for each entry,
whether a [`deploy`](/cli/scheduler/commands/deploy) would create it, update it — naming the fields that differ —
or leave it alone. It writes no schedule, no file, and no session.

It is the command to run before a deploy, and the command to run when a deploy has surprised you.

## Synopsis

```bash theme={null}
blobhub scheduler diff -f <manifest>
```

| Flag           | Default  | Description                     |
| -------------- | -------- | ------------------------------- |
| `--file`, `-f` | required | Path to the scheduler manifest. |

There is no `--dry-run` here. `diff` has no side effect to withhold — and the difference between the two commands
is exactly that, so `deploy --dry-run` is not a synonym for this one. See
[It never creates a session](#it-never-creates-a-session) below.

## What it reports

Drift first, then one line per manifest entry, then the sessions a deploy would have to create:

```text theme={null}
drift: 2 schedule(s) on the revision are not in the manifest and were left untouched -- they are still firing on their configured schedule: weekly_cleanup, 419365d8-a798-49dd-ad29-5f97a9c1b901 (the removal path is 'blobhub scheduler prune', not yet available); 1 of them have no alias at all and cannot be declared in a manifest, only removed
flash_sale_launch: create
daily_checkout_report: update (cron_expression)
flash_sale_launch: session 'flash_sale_2026' would be created by deploy
```

| Line                                                   | Meaning                                                                                              |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
| `<alias>: create`                                      | The manifest declares it; the revision has no schedule with that alias.                              |
| `<alias>: update (<fields>)`                           | Both sides have it, and these fields differ.                                                         |
| `<alias>: unchanged`                                   | Both sides have it and every compared field matches. A `deploy` would write nothing for it.          |
| `<alias>: session '<name>' would be created by deploy` | The entry's `target.session` alias does not exist yet. Reported, never created.                      |
| `drift: …`                                             | Schedules on the revision that no manifest entry claims. They are left alone — and they keep firing. |

Under `--json` the payload carries `blob`, `create`, `update` (each with `alias` and `fields`), `unchanged`,
`sessions_to_create` and `drift`, alongside the envelope's `schema_version`.

The field names in an `update` line are the **manifest's** vocabulary, not the platform's: `target.session`,
`target.revision`, `target.workflow`, `target.input_data`, `target.description`, alongside `repeat`, `timezone`,
`enabled`, `start_date`, `end_date` and whichever timing field applies. They name the lines in your own file you
would have to change to make the difference go away.

## It never creates a session

This is the one property that makes `diff` safe to run anywhere, and the one real difference between it and
`deploy --dry-run`.

Both commands must resolve every target before they can say anything useful — a comparison against unresolved
symbols would be comparing a manifest to nothing. But `deploy` resolves a `session:` alias by **finding or
creating** it, and it does that under `--dry-run` too. `diff` uses a preview resolver instead: it looks the
session up, and when it is simply absent it reports `would be created by deploy` and moves on.

|                                                       | `diff` | `deploy --dry-run` |
| ----------------------------------------------------- | ------ | ------------------ |
| Resolves the target blob, revision and workflow alias | Yes    | Yes                |
| Creates a missing `target.session`                    | **No** | **Yes**            |
| Writes a schedule                                     | No     | No                 |

So a manifest naming a session that does not exist yet can be diffed as many times as you like without leaving
anything behind. Dry-running the same manifest twice creates the session on the first run.

Everything else the two resolvers do is shared code, so they cannot drift apart on what a target resolves to. A
session that exists but is **closed** is refused by both, identically — `diff` reporting a clean update for an
entry `deploy` was always going to refuse would be worse than failing.

## Only the timing field that applies is compared

`repeat` decides which of `cron_expression` and `invocation_time` has meaning, and only that one is compared.

The other is not merely irrelevant, it is **unclearable**. `update_schedule` sets only the keys it is sent and
rejects a `null`, so a schedule flipped from `recurring_cron` to `one_time` keeps its old `cron_expression` on the
record for good. Comparing both regardless would report `update (cron_expression)` on every diff, and re-issue an
update on every deploy, for as long as the entry existed — over a field nobody can remove.

A manifest entry carrying both fields is refused at load as `MANIFEST_INVALID`, which is why this only ever
concerns the stored record.

## Unlike `workflow diff`, this one can fail

[`blobhub workflow diff`](/cli/workflow/commands/diff) always exits `0`: every difference it can discover,
including a package that does not compile, is printed rather than raised. `scheduler diff` does not make that
promise, and the reason is that it validates targets.

Reporting that an entry would be created is a claim about what `deploy` will do. If the entry's target blob does
not resolve, or its `workflow:` names nothing on the target revision, or its session is closed, then `deploy` is
not going to create anything — so `diff` raises the same code `deploy` would, rather than blessing an entry that
is already broken:

```text theme={null}
error: SCHEDULE_TARGET_INVALID: workflow alias 'checkout_flow_v2' is not a workflow-category definition on revision 3d1317ed-... -- workflow_alias is late-bound at fire time, so a rename or removal would otherwise break this schedule silently
```

With a key that cannot read the target blob's limits — which is most keys, since limits need `admin` — that
message gains a clause saying the definition listing could not be confirmed complete. It still refuses: a typo is
far likelier than a revision at its definition cap.

What `diff` still never fails on is a **difference**. Every create, update and unchanged verdict, and every
drifted schedule, is a finding and the exit code stays `0` — so it is not a CI gate for "the manifest is
deployed". Read the `--json` payload for that.

## Drift is reported, never resolved

Schedules the manifest does not cover are listed and left alone. Two things make that worth acting on here rather
than filing away:

* An orphaned schedule is not inert. It keeps firing on its cron, consuming execution quota and appending to its
  session on every fire.
* An orphaned schedule **with no alias** cannot be adopted by any manifest at all, so the drift line calls those
  out separately, by id. [`ls`](/cli/scheduler/commands/ls) explains what to do about one.

[`pull`](/cli/scheduler/commands/pull) is what adopts aliased drift; nothing in this group removes drift today.

## Errors

| Code                                                                                               | When                                                                                                                                                                                                                                                                                                           |
| -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MANIFEST_NOT_FOUND`, `MANIFEST_INVALID`, `MANIFEST_TYPE_MISMATCH`, `MANIFEST_VERSION_UNSUPPORTED` | The manifest is absent or unusable — including an entry carrying both or neither of `cron_expression`/`invocation_time` for its declared `repeat`.                                                                                                                                                             |
| `SCHEDULE_ALIAS_REQUIRED`, `SCHEDULE_ALIAS_INVALID`                                                | An entry has no `alias`, or one that fails `^[a-z0-9_-]{6,42}$`.                                                                                                                                                                                                                                               |
| `SCHEDULE_TARGET_INVALID`                                                                          | An entry's target blob, target revision, or `workflow:` alias does not resolve. The alias is checked against the target revision's `workflow`-category definitions only — a `playground` definition of the same name does not count.                                                                           |
| `SESSION_NOT_ACCESSIBLE`, `SESSION_NOT_OPEN`                                                       | A `target.session` **id** does not resolve — nobody can mint one client-side, so an unresolvable id is a genuine failure even here — or the session it names is closed. A missing session *alias* is not an error: it is reported as one `deploy` would create.                                                |
| `BLOB_NOT_ACCESSIBLE`, `BLOB_NOT_WORKFLOW`, `REVISION_NOT_ACCESSIBLE`, `REVISION_NOT_WRITABLE`     | The scheduler blob cannot be resolved, is a blob of another domain, or its revision is unreachable or not `ready`. This is a read, so the phase may be `draft`, `commit` or `managed` — anything else raises `REVISION_NOT_WRITABLE`, which the shared resolver raises on the read path too, despite the name. |
| `FORMAT_UNSUPPORTED`, `FORMAT_PARSE_ERROR`                                                         | The manifest has an unsupported extension, or does not parse.                                                                                                                                                                                                                                                  |
| `CREDENTIALS_NOT_FOUND`, `PROFILE_NOT_FOUND`, `INSECURE_CREDENTIALS_PERMISSIONS`                   | No usable credential, or a credentials file the CLI refuses to read.                                                                                                                                                                                                                                           |
| `API_RATE_LIMITED`, `API_TRANSIENT_ERROR`, `API_NETWORK_ERROR`                                     | A call failed after its retries.                                                                                                                                                                                                                                                                               |
| `DEFINITION_LIST_TRUNCATED`                                                                        | Advisory, on stderr. The target revision's definition listing reached its configured cap, so the `workflow:` alias could not be confirmed either way and the check was surrendered to the platform. The exit code stays `0`.                                                                                   |

Every code above, with its remediation, is in [Error codes](/cli/error-codes).

## See also

* [`blobhub scheduler deploy`](/cli/scheduler/commands/deploy) — what `diff` is describing, and the only command
  here that writes.
* [`blobhub scheduler ls`](/cli/scheduler/commands/ls) — the revision's schedules, including the ones no manifest
  can name.
* [Scheduler manifest](/cli/scheduler/manifest) — every field this comparison covers.
* [Scheduler overview](/cli/scheduler/overview) — why `--dry-run` belongs to `deploy` alone.
