Skip to main content
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: Grouped by blob domain, each invoked as blobhub <domain> <command>: workflow and scheduler 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 typeworkflow_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. 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: 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 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