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
workspaceentries 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 = trueor when it has members but nomain(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.