eject downloads a definition from the revision and writes its logic.code content back out as files on disk.
It exists for one situation: a workflow that was authored in the visual editor and now needs to live in a
repository.
It is not the inverse of build. The code stored on the revision is the
compiled code — the compiler rewrote every import into a flat-namespace rebinding (import numpy as np became
np = numpy) and that transform has no inverse. What eject recovers is whatever structure the compiler’s
source markers preserved, never the import statements they replaced. The command prints that caveat on every run
that renders human output — it is part of the rendered form, so --json never carries it:
build and diff until the compiled output matches what was there.
Synopsis
There is no
--dry-run. --force is the guard instead: without it, a run that would overwrite anything writes
nothing at all.
What it does
- Resolves the blob and revision. This is a read, so a
commit-phase revision is fine. - Lists the revision’s definitions and matches them against the manifest.
- For each manifest entry that has a remote counterpart, downloads the definition and, for each declared
component binding, reads its
codeport content and splits it into files. - Checks every destination path for a conflict before writing any of them.
- Writes the files, prints one line per file, and prints a manifest snippet for each component that has no binding yet.
components[] at all. eject only ever looks at bindings you have written
down, so adopting a component means naming it in the manifest first, even when it has no code: block yet.
Under --json the payload carries definitions — each with its alias, its destination root and the files
written — plus bound, alongside the envelope’s schema_version. bound is true when the run printed no
manifest snippets: nothing needs adding to the manifest.
Where the files land
The destination depends on whether the component is already bound:
The rejection is deliberate. A bound component already has a root that
build compiles from; accepting a second
one would put the package in two places and leave build compiling the one you stopped editing.
That split matches the two reasons to run this command. A bound component ejects into its own base_path —
this is the recovery path out of REMOTE_EDIT and UNMANAGED_CODE, where someone edited the code in the browser
and you need it on disk to reconcile by hand. An unbound component ejects into a fresh directory, because
creating that binding is what adoption is, and the command prints the snippet to paste:
entry_point is the last file reconstructed, not the first. The compiler emits local modules
in dependency-first order and preserves that order into content[], so the entry point — the module nothing else
in the package imports — is always last.
--output names one root for the whole run. Ejecting several unbound definitions into it at once is a mistake:
two definitions whose markers share a relative path would land on the same file.
What the markers preserve
The compiler prefixes each module it emits with# blobhub:source <relative path>. eject reads those markers
back:
- Text under a marker becomes that marker’s file, at that relative path, nested directories included.
- Text with no marker above it becomes one fallback file, named
<alias>_<name>.py— where<name>is the binding’sname, or the literalcodewhen the binding names its component byid. This is the browser case: code typed into the visual editor has no markers at all, so a definition that was never built by this CLI ejects as exactly one file. - A marker with an empty body still produces an empty file. An empty
__init__.pycompiles to its marker line and nothing else, and dropping it as “no content” would turn a package into a broken import. - Non-text parts of
content[]are ignored.
.. is refused with BASE_PATH_ESCAPE rather than allowed to escape the
destination root.
A binding that names a component with no code port contributes no files and no error. If a definition
ejects nothing, check that the binding points at the component you meant.
Nothing is written until every path is clear
eject collects every destination first and checks all of them for an existing file. If any exists and
--force was not passed, it raises EJECT_TARGET_EXISTS, naming every conflicting path, and nothing is
written — not the files that would not have conflicted, not a partial directory tree. The same holds for the
--output-on-a-bound-component refusal. A refused run leaves the filesystem exactly as it was.
With --force, existing files are overwritten. There is no backup; a checkout with uncommitted changes is the
only thing standing between --force and losing them.
Errors
Every code above, with its remediation, is in Error codes.
See also
blobhub workflow build— the compile that produced the markers, and the gate whose refusals send you here.blobhub workflow pull— the definition document, verbatim, which is a different thing from its code.blobhub workflow diff— how you confirm the reconstructed package compiles back to what is deployed.- Component code — the port
ejectreads.

