Skip to main content

Workspaces

A Leo workspace groups multiple Leo packages (programs or libraries) under a single root directory. When you run commands like leo build or leo test from the workspace root, Leo operates on every member in the correct dependency order - no manual sequencing required.

Workspaces are useful when your application is made up of several interacting programs. For example, a DeFi protocol might have a token program and a swap program that depends on it. A workspace lets you build, test, and clean them all with a single command.

Creating a Workspace

The quickest way to start a workspace is to scaffold one with leo new:

leo new --workspace my_project

This creates my_project/ containing a workspace.json with an empty members array. The --workspace flag is mutually exclusive with --library - a workspace is just a root that groups packages, not a package itself, so it has no src/, program.json, or tests/ directory.

Equivalently, you can create workspace.json by hand in any directory. It contains a members array listing the relative paths to each member package:

{
"members": ["token", "swap"]
}

Each entry is a path to a directory containing a standard Leo package with its own program.json and src/main.leo.

Glob Members

Entries in members can also be glob patterns, resolved relative to the workspace root:

{
"members": ["libraries/core", "programs/*"]
}

Standard glob syntax is supported, including * (matches a single path segment), ** (matches recursively across directories), ?, and character classes like [abc]. A glob match is included only if the matched directory contains a program.json; other matches (files, directories without a manifest, non-UTF-8 paths) are silently skipped.

A glob that matches zero packages logs a warning and continues - it is not an error:

workspace member glob `programs/*` in <root> matched no packages

A literal entry pointing at a missing directory still errors, so explicit paths remain strictly validated. Literal entries are resolved before globs and members are deduplicated by canonical path, so a directory matched by both a literal entry and a glob is only included once.

Adding Members

When leo new <name> is run anywhere inside a workspace, the new package's path is appended to the enclosing workspace.json automatically and Leo prints:

Added <name> to the enclosing workspace.

The append is skipped silently if the new package is already covered by an existing entry - either a literal path equal to the new package's relative path, or a glob that matches it. If the new package ends up outside the discovered workspace root (for example, leo new ../sibling), Leo prints a warning and leaves workspace.json untouched:

new package at `...` is not inside the discovered workspace root `...`; skipping auto-add

Existing members order is preserved; new entries are appended at the end. If you would rather not edit members by hand at all, use a glob entry such as programs/* (see Glob Members) - new packages created inside that directory are picked up without modifying workspace.json.

Directory Structure

A typical workspace looks like this:

my_project/
├── workspace.json
├── token/
│ ├── program.json
│ └── src/
│ └── main.leo
└── swap/
├── program.json
└── src/
└── main.leo

Each member is a self-contained Leo package. It has its own build/ directory when compiled.

Member Dependencies

Members can depend on each other using workspace dependencies. For example, if swap depends on token, add the dependency with:

cd swap/
leo add --workspace token

This writes a workspace dependency entry to the member's program.json:

swap/program.json
{
"program": "swap.aleo",
"version": "0.1.0",
"description": "",
"license": "MIT",
"dependencies": [
{
"name": "token.aleo",
"location": "workspace",
"path": null,
"edition": null
}
],
"dev_dependencies": null
}

A workspace dependency uses "location": "workspace" and requires no path - Leo automatically resolves the member's location from workspace.json at build time.

Alternatively, members can use local path dependencies with "location": "local" and an explicit relative path. Workspace dependencies are preferred because they are shorter to write and do not break if you reorganize directories.

Build Order

Leo automatically determines the correct build order by analyzing the dependency graph across workspace members. When one member depends on another, Leo ensures the dependency is built first.

In the example above, token has no dependencies and swap depends on token, so Leo builds token first, then swap. This ordering is computed automatically regardless of the order members are listed in workspace.json.

If the dependency graph contains a cycle (e.g., A depends on B and B depends on A), Leo reports an error rather than attempting to build.

Working with Workspaces

Building

From the workspace root, leo build compiles all members in dependency order:

leo build

From inside a member directory, it builds only that member:

cd token/
leo build

Testing

From the workspace root, leo test runs test suites for all members:

leo test

From inside a member directory, it tests only that member:

cd swap/
leo test

Cleaning

From the workspace root, leo clean removes build artifacts for all members:

leo clean

From inside a member directory, it cleans only that member:

cd token/
leo clean

Deploying

From the workspace root, leo deploy deploys all members in dependency order using a shared VM for accurate fee estimation. Programs shared across members are deployed only once:

leo deploy --broadcast

From inside a member directory, it deploys only that member:

cd swap/
leo deploy --broadcast

Targeting a Specific Member

Use the --package (or -p) flag to target a specific member from anywhere within the workspace:

leo build -p swap
leo test --package token
leo deploy --broadcast -p swap
leo clean -p swap

The flag accepts any of:

  • The member's directory name (e.g., token)
  • The program name with .aleo suffix (e.g., token.aleo)
  • The program name without suffix (e.g., token)

If the name does not match any workspace member, Leo reports an error and suggests checking the members list in workspace.json.

Example

Here is a minimal workspace with two members. The token program exposes a mint function:

program token.aleo {
fn mint(owner: address, amount: u32) -> u32 {
return amount;
}

@noupgrade
constructor() {}
}

The swap program imports and calls into token:

import token.aleo;

program swap.aleo {
fn do_swap(owner: address, amount: u32) -> u32 {
return token.aleo::mint(owner, amount);
}

@noupgrade
constructor() {}
}

Building from the workspace root compiles token first (no dependencies), then swap.

Backward Compatibility

Projects without a workspace.json continue to work exactly as before. Workspaces are purely opt-in - Leo only activates workspace behavior when it discovers a workspace.json by walking up the directory tree from the current working directory.