Documentation menu

Guides

Workspaces

Last updated August 24, 2026

One repository, many packages — pesde-style. A manifest whose [target] has a workspace array is a workspace root:

# ember.toml at the repository root
[package]
name = "chief/root"
version = "0.0.0"
private = true                 # the root itself never publishes

[target]
environment = "shared"
workspace = ["packages/*"]     # members; no `main` needed

Members#

  • The workspace entries are globs relative to the manifest: * stays within one path segment, ** crosses, a ! prefix subtracts matches, and a literal . includes the root itself.
  • Every matched directory must contain an ember.toml (error otherwise).
  • Members are complete, independent Ember projects. Nested workspaces are not a thing — a member's own members are never iterated from the outer root.

Older manifests may still use workspace_members; it remains accepted as a parsing alias, but write workspace.

Members depending on each other#

# packages/traits/ember.toml — a member depending on a sibling
[dependencies]
core = { workspace = "chief/core", version = "^" }

workspace means two different things. On the workspace root, [target] workspace = ["packages/*"] is an array of globs declaring the members. In a member's [dependencies], dep = { workspace = "scope/name" } is a dependency specifier pointing at another member by package name.

During development the version is ignored — you always get the member's current code. Install links the dependency live: the link file requires the member's source directory directly (return require("../../../core/src/init"), type re-exports included), so edits to the member are picked up without reinstalling.

  • The member's own dependencies (registry and workspace alike) flatten into the consumer's install set for that root; its registry dependencies resolve against the member's [indices].
  • A member owns its name in every environment root and always links in place, so a server-tree dependency on a member binds to your local copy rather than the published package — members are the one thing that is never duplicated per root. A version requirement the member can't satisfy is a conflict, not a silent fallback to the registry.
  • Ember finds the workspace by walking up from the project: the first ancestor manifest whose member globs match the project's directory is the root. A workspace dependency outside any workspace is an error.

Installing at the root#

embr install at the root installs the root project first, then every member — each into its own packages folders, each writing its own lockfile.

Workspaces never take install's fast path, so a workspace install always does the full pass.

Publishing a workspace#

embr publish at the root publishes the root first, then every member:

  • The root skips itself when private = true or when it has members but no main (a pure container) — members still go out.
  • One member failing doesn't stop the rest; failures are printed per package and the command exits non-zero at the end listing them.

At publish time, workspace specifiers become registry requirements. The version field is a version type applied to the member's current on-disk version — ^ → ^1.2.3, ~ → ~1.2.3, = → =1.2.3, * → any — or a full requirement (^2.1.0) passed through unchanged. The default is ^. The rewrite happens only in the uploaded archive's manifest; the on-disk ember.toml is never modified.

  • Publishing from inside a member publishes only that member (workspace dependencies still convert via the ancestor root).

Hooks in a workspace#

Each manifest's hooks bracket its own portion, and run in its own directory. embr install at the root goes root preinstall → root install → each member's own preinstall/install/postinstall → root postinstall, so a root postinstall sees a fully installed workspace. embr publish has the same shape. See Lifecycle hooks.