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

# Concepts

> The CLI mental model: shared and domain commands, manifests, the compiled/verbatim split, and drift

This page covers what you need before running anything: how the command surface is split, what a manifest
describes, the difference between the commands that move code and the ones that compile it, and the handful of
platform behaviours that shape every command in the binary.

## Shared and blob-domain commands

Everything that is not tied to a particular kind of blob is shared. Everything else lives in a command group
named after a **blob domain**, which is the axis the platform itself extends along.

Shared, and so usable whatever you are working on:

| Group                       | Surface                                                              |
| --------------------------- | -------------------------------------------------------------------- |
| Authentication and profiles | `login`, `logout`, `whoami`, `profile ls`, `profile use`             |
| Blob inspection             | `blob show`, `blob revisions`, `blob limits`                         |
| Utilities                   | `doctor`, `completion`                                               |
| Global flags                | `--json`, `--verbose` / `-v`, `--quiet` / `-q`, `--org`, `--version` |

Grouped by blob domain, each invoked as `blobhub <domain> <command>`:

| Domain      | Commands                                                                              |
| ----------- | ------------------------------------------------------------------------------------- |
| `workflow`  | `pull`, `push`, `build`, `deploy`, `diff`, `check`, `eject`, `execute`, `ls`, `prune` |
| `scheduler` | `ls`, `pull`, `diff`, `deploy`                                                        |

`workflow` and [`scheduler`](/blob-types/scheduler/overview) are the two domains today. A new domain arrives as
another command group, with its own commands and its own manifest type over the same shared base; nothing on the
shared side changes.

`blob show`, `blob revisions` and `blob limits` sit on the shared side rather than becoming a third domain
because they work on a blob whatever its domain. They answer "what am I pointing at, and may I write to it?",
which is the question you ask before choosing a domain group at all.

## A manifest describes one revision, not one definition

A revision is what the platform versions and commits, so that is the scope a manifest is written at. Its shared
base names the blob and the revision; its body lists everything that belongs on that revision — every definition
in the `workflow` case, across both categories, and every schedule in the `scheduler` case.

There is no per-definition manifest. The file a manifest's `source` points at is the definition document itself,
not a manifest of its own.

## Category participates in identity

A workflow definition is identified by the pair **`(category, alias)`**, not by the alias alone. The platform's
own uniqueness sentinel is keyed on both, so one alias may legitimately name a `workflow` definition *and* a
`playground` definition on the same revision. That is a supported state, not an anomaly.

The CLI therefore keys its definition set on the pair. A bare `--definition <alias>` still takes a bare alias,
and if it matches an entry in both categories it operates on **both** rather than erroring.

`category` is inferred from each source document's own `type` — `workflow_definition` gives `workflow`,
`playground_definition` gives `playground` — and may be overridden per manifest entry. A declared category that
contradicts the document is an error, because nothing server-side catches it: a workflow upload validates nothing
at all, so a playground document declared as a workflow uploads cleanly into the workflow definition's id and
corrupts it silently.

## Verbatim and compiled

Four of the workflow commands move a definition document; only two of them know that Python exists.

| Command  | What it moves                          | Code                                                                                         |
| -------- | -------------------------------------- | -------------------------------------------------------------------------------------------- |
| `pull`   | platform → local definition file       | Verbatim and code-unaware: whatever is in the code ports is written as it stands.            |
| `push`   | local definition file → platform       | Verbatim: the document is uploaded as it stands, with no compiler in the path.               |
| `build`  | Python package → local definition file | Compiles each bound component and writes the result into the file. Purely local; no network. |
| `deploy` | Python package → platform              | `build`, then upload only what changed, then check what was uploaded.                        |

`pull` and `push` are a symmetric pair, and a document that round-trips through them comes back byte-symmetric.

The local definition file is always a **complete, uploadable document**. Its code ports hold a committed build
output — the compiled content itself, not a reference to the package that produced it. That is why `push` needs
no compiler, why `pull` never needs to know a package exists, and why the compiled code is reviewable in a pull
request alongside the sources it came from.

## Managed code ports

Every code port the CLI writes carries a `content_hash` in the message value's `attributes`: SHA-256 over the
canonical compiled content. `content_hash` does not exist anywhere in the platform — the CLI introduces it, the
platform stores it blind, and nothing server-side ever reads or interprets it.

It is what lets `build` refuse to overwrite code that was edited in the visual editor:

| Recorded `content_hash`            | State                             | Behaviour                                                        |
| ---------------------------------- | --------------------------------- | ---------------------------------------------------------------- |
| Present, matches the embedded code | Built by the CLI, untouched since | Overwritten silently.                                            |
| Present, does not match            | Edited after the last build       | `REMOTE_EDIT` — pass `--force`, or reconcile with `eject` first. |
| Absent                             | Never built by the CLI            | `UNMANAGED_CODE` — same two ways out.                            |

`diff` reports the same three states without writing anything.

The scheme holds whichever way the visual editor treats an attribute it does not recognize. If it preserves the
attribute, the hash goes stale and mismatches; if it regenerates the message envelope, the hash disappears. Both
stop and tell you, so the CLI never depends on the editor cooperating.

## Drift is reported, never acted on

Definitions present on the revision but absent from the manifest are **drift**. No command acts on them: they are
left untouched, and the command still exits `0`. How each one *reports* them varies — most print a one-line
`drift:` summary, `workflow ls` marks the drifted rows in its table instead, `workflow build` never talks to the
platform and so cannot see them, and `workflow eject` and `workflow execute` do not mention them at all. The
[Workflow overview](/cli/workflow/overview) has the command-by-command breakdown. Deletion is never implied by a
deploy: `workflow prune` is the one command that removes them, deliberately and only when asked.

Schedules work the same way, with one difference worth stating — drift there is not inert. A schedule the
manifest no longer covers keeps firing on its cron and keeps consuming execution quota.

## Four platform behaviours that shape everything

Each of these is non-guessable, and each explains a message that otherwise reads as a bug.

### Absent and inaccessible are the same `403`

A definition that does not exist and one that lives in a revision you cannot reach both come back as
`403 forbidden`, never `404`. The platform cannot distinguish the two, so neither can the CLI: every message it
emits for a server-addressed object reads *absent or not accessible*. That is a report of what the platform said,
not hedging.

### `list_definitions` cannot page

The listing that resolves alias → id has no cursor and no limit. It is a single query, so a revision holding
enough definitions to exceed one page **silently truncates**, and nothing in the API reports it. The CLI detects
the likely case by comparing the returned count against the revision's configured maximum and raises the
`DEFINITION_LIST_TRUNCATED` advisory. It can only do that when the limit is readable, which needs an
`admin`-scoped key; otherwise it stays silent rather than guessing.

### Reading the import allowlist requires an `admin` key

A blob's `workflow_component_code_imports` allowlist — the third-party modules a revision's code may import — is
part of the blob's limits, and reading limits requires an `admin`-scoped key. The `read` and `write` keys the
platform recommends for automation are not that.

So when the allowlist cannot be read, an unresolved import degrades from the hard failure `IMPORT_UNAVAILABLE` to
the advisory `IMPORT_UNVERIFIED`: the build proceeds and tells you it could not verify the import. Pass
`--strict-imports`, an option on `workflow build` and `workflow deploy`, to restore certainty and fail instead.

### A check can fail with HTTP `200`

`check_definition` returns HTTP `200` carrying `{"status": "failure", "events": [...]}` in its body. The CLI keys
off the body's `status` and never off the HTTP status — a tool that gates on the HTTP status alone reports a
broken workflow as valid.

## See also

* [Introduction](/cli/introduction)
* [Error codes](/cli/error-codes) — the full catalog, including the advisories the four behaviours above produce.
* [Workflow definition format](/blob-types/workflow/workflows/definition-format)
* [Component code](/blob-types/workflow/workflows/component-code)
* [Scheduler](/blob-types/scheduler/overview)
