> ## 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 workflow build

> Compile each bound component locally and write the result into the definition file — no network

`build` turns your Python package into the flat, import-less namespace the platform's sandbox executes, and
writes it into the definition file's code port in place. It is the compiler step of
[`deploy`](/cli/workflow/commands/deploy), available on its own.

**It makes no network calls of any kind.** It never resolves a blob, never lists definitions, and never resolves
a credential — `build` runs in a checkout with no API key, no credentials file, and no connectivity. That is
what makes it usable as a pre-commit hook and as the first, cheapest step in CI.

## Synopsis

```bash theme={null}
blobhub workflow build -f <manifest> [--definition <alias>] [--force] [--strict-imports] [--dry-run]
```

| Flag               | Default                          | Description                                 |
| ------------------ | -------------------------------- | ------------------------------------------- |
| `--file`, `-f`     | required                         | Path to the workflow manifest.              |
| `--definition`     | every definition in the manifest | Limit to this alias. Repeatable.            |
| `--force`          | off                              | Overwrite code edited outside the CLI.      |
| `--strict-imports` | off                              | Treat an unverifiable import as fatal.      |
| `--dry-run`        | off                              | Report the intended calls and perform none. |

## What it does

For each manifest entry that declares `components[]` — an entry with no bindings is skipped entirely, since
there is nothing local to compile into it:

1. Reads the entry's `source` document. A missing file is `MANIFEST_INVALID`, hinting at
   [`pull`](/cli/workflow/commands/pull).
2. Resolves the entry's category from the document's own `type`, by the same rule every other command uses, so a
   manifest cannot build clean and then fail on the first command that talks to the platform.
3. Binds each component by `id` or unique `name`, and for a `code` binding: checks the component's `language`
   port is `python`, runs the remote-edit gate below, compiles `entry_point` under `base_path`, and writes the
   resulting `content[]` into the `code` port.
4. For a `value: {source}` binding, reads that JSON/YAML file and writes it into the component's `value` port.
5. Writes the document back to `source` in its own format, and prints one line per definition:

```text theme={null}
built workflow/checkout_flow
```

Under `--json` the payload carries `built`, `planned` and `dry_run` alongside the envelope's `schema_version`.
There is no `drift` key here, unlike every other command in this group: `build` never lists the revision, so it
has nothing to compare the manifest against.

## Deterministic by construction

Compilation is a pure function of the sources. The compiled content is hashed (SHA-256 over its canonical JSON)
and that hash becomes both the port's `content_hash` and, through a `uuid5` derivation, the message's `id`. The
writer touches no other key and reorders nothing.

So a rebuild over unchanged sources leaves the definition file **byte-identical**, and `git status` stays clean.
That is the property that makes the compiled code reviewable: a diff in the definition file means a real change
in the package.

One exception, once per port: `created_at` is carried forward from whatever is already in the file, and stamped
with the current time only when there is nothing to carry. A code port the CLI has never built therefore changes
on its first build and is stable from the second onward.

## The remote-edit gate

Before overwriting a code port that already has content, `build` classifies it:

| Recorded `content_hash`      | State                             | Behaviour                                                                       |
| ---------------------------- | --------------------------------- | ------------------------------------------------------------------------------- |
| Matches the embedded code    | Built by the CLI, untouched since | Overwritten silently.                                                           |
| Does not match               | Edited since the last build       | `REMOTE_EDIT` — the command stops there.                                        |
| Absent, or the port is empty | Never built by this CLI           | `UNMANAGED_CODE`, unless the port is empty, which is written without complaint. |

`--force` skips the classification entirely. The other way out is `blobhub workflow eject`, which brings the
edited code onto disk so you can reconcile it by hand first.

A refusal stops the whole command, not just that definition. Definitions built earlier in the same run keep the
content they were given — the file is written per definition, as each one finishes.

**The gate reads the code port in the local definition file**, because that is the only document `build` has.
An edit made in the visual editor reaches that file when you [`pull`](/cli/workflow/commands/pull);
[`diff`](/cli/workflow/commands/diff) is what reports the remote's state without pulling. Run one of the two
before a build you intend to deploy.

## Imports and the allowlist

The compiler classifies every import as local, sandbox pre-bound, blob-allowlisted, or unavailable. The
allowlist — a blob limit — needs an `admin`-scoped key to read, which `build` does not have and could not use
offline anyway, so it reads the local cache under `~/.blobhub/cache/allowlist/`, keyed on the manifest's own
`blob:` reference.

With no cache entry, `ALLOWLIST_UNREADABLE` is raised as an advisory and an unresolved import degrades from the
hard failure `IMPORT_UNAVAILABLE` to the advisory `IMPORT_UNVERIFIED` — the build proceeds and says it could not
verify. `--strict-imports` promotes that back to a refusal, which is what a CI job should set. A cache entry
older than seven days additionally raises `ALLOWLIST_STALE`. Two commands write that cache, both needing an
`admin` key: [`deploy`](/cli/workflow/commands/deploy), which refreshes it live on every run, and
[`blobhub blob limits --refresh`](/cli/blob/limits).

Everything that does not depend on the allowlist is enforced regardless: local module resolution, import cycles,
symbol collisions, and names the sandbox pre-binds.

The classification rules, the import-rewrite table and the compiler's documented limitations are in the
[Compiler](/cli/workflow/compiler) reference; what the allowlist extends — the pre-bound modules and names — is
in the [Sandbox](/cli/workflow/sandbox) reference.

## `--definition` here is not the same check

Eight commands in this group take `--definition`; the seven other than `build` resolve it while loading the
revision's definition set, and reject an alias the manifest does not carry. `build` filters the manifest list
directly, so **an alias that matches nothing is a silent no-op**: no error, no output, exit `0`. Check the
spelling against the manifest if a build reports nothing built.

## `--dry-run`

Reads each document and resolves its category, then reports what it would build:

```text theme={null}
would build workflow/checkout_flow
```

It does **not** compile, so a dry run cannot report a compile error, an unresolved import, or a remote edit. It
answers which definitions are in scope, not whether they would succeed.

## Errors

| Code                                                                                                                                                              | When                                                                                                                                       |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `MANIFEST_NOT_FOUND`, `MANIFEST_INVALID`, `MANIFEST_TYPE_MISMATCH`, `MANIFEST_VERSION_UNSUPPORTED`                                                                | The manifest is absent or unusable — including a bound entry whose `source` file does not exist.                                           |
| `DEFINITION_ALIAS_INVALID`, `DEFINITION_CATEGORY_UNKNOWN`                                                                                                         | An entry's alias fails the pattern; or its category cannot be inferred and none is declared, or the declared one contradicts the document. |
| `FORMAT_UNSUPPORTED`, `FORMAT_PARSE_ERROR`                                                                                                                        | A `source` or `value.source` has an unsupported extension, or does not parse.                                                              |
| `COMPONENT_NOT_FOUND`, `COMPONENT_NAME_AMBIGUOUS`, `COMPONENT_PORT_MISSING`                                                                                       | A binding matches no component, matches more than one by `name`, or the component has no `code`/`value` port.                              |
| `LANGUAGE_NOT_PYTHON`                                                                                                                                             | The bound component's `language` port is not `python` — including a component with no `language` port at all.                              |
| `REMOTE_EDIT`, `UNMANAGED_CODE`                                                                                                                                   | The gate above refused the port. Pass `--force`, or reconcile with `blobhub workflow eject` first.                                         |
| `ENTRY_POINT_NOT_FOUND`, `BASE_PATH_ESCAPE`                                                                                                                       | The `entry_point` does not exist under `base_path`, or a module resolves outside it.                                                       |
| `IMPORT_UNAVAILABLE`, `IMPORT_NAME_NOT_FOUND`, `IMPORT_CYCLE`, `SYMBOL_COLLISION`, `SANDBOX_NAME_SHADOWED`, `LOCAL_MODULE_IMPORT_FORM`, `UNSUPPORTED_IMPORT_FORM` | The compiler refused the package. Each names the module and the statement.                                                                 |
| `ALLOWLIST_UNREADABLE`, `ALLOWLIST_STALE`, `ALLOWLIST_ENTRY_UNUSABLE`, `ALLOWLIST_SHADOWS_SANDBOX`, `IMPORT_UNVERIFIED`, `LOCAL_SHADOWS_SANDBOX_MODULE`           | Advisories, on stderr. The exit code stays `0`.                                                                                            |

No credential or transport code can surface here, because no call is made. If `build` reports
`CREDENTIALS_NOT_FOUND`, you are running a different command.

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

## See also

* [`blobhub workflow deploy`](/cli/workflow/commands/deploy) — the same compile, followed by the upload and the
  check.
* [`blobhub workflow diff`](/cli/workflow/commands/diff) — compiles in memory and reports, writing nothing.
* [`blobhub blob limits`](/cli/blob/limits) — the import allowlist and the key that can read it.
* [Component code](/blob-types/workflow/workflows/component-code) — what the sandbox binds and what it executes.
