Configuration
lattice.json at the repo root is the only file Lattice reads for
configuration. This page lists every field it accepts, with its type, whether it
is required, its default, and what an invalid value does. For what the fields
mean in practice, see Workspaces, Task
graph, Caching, and Engines
and provisioning.
Top-level fields
Every top-level key is optional, so {} is a valid lattice.json.
| Field | Type | Required | Default |
|---|---|---|---|
$schema |
string |
no | none |
latticeVersion |
string |
no | none |
workspaces |
array of workspace objects | no | [] |
engines |
engine map | no | {} |
globalDependencies |
array of string |
no | [] |
globalEnv |
array of string |
no | [] |
tasks |
object of string to task object |
no | {} |
settings |
settings object | no | all defaults |
Where the file lives
Every command that reads config walks up from the current directory to the
nearest lattice.json and treats that directory as the repo root. Commands
therefore work from any subdirectory. With no lattice.json in the current
directory or any parent, the command fails before reading anything else:
Error: no lattice.json found in this directory or any parent. Run `lattice init` to create one
$schema
{ "$schema": ".lattice/schema.json" }
| Field | Type | Required | Default |
|---|---|---|---|
$schema |
string |
no | none |
A plain string reference, conventionally .lattice/schema.json. That path holds
a copy of Lattice’s bundled JSON Schema, written next to your config so that
editors with JSON Schema support validate and autocomplete lattice.json as you
type. Lattice never reads the value.
lattice run, lattice setup, lattice prune, and lattice stats write
.lattice/schema.json only when the file is absent, so a copy you have pinned
or hand-edited stays as it is. lattice init writes it unconditionally as part
of scaffolding, along with five .gitignore lines: .lattice/cache/,
.lattice/toolchains/, .lattice/bin/, .lattice/setup/, and
.lattice-setup-marker.
.lattice/schema.json is not among them.
Unknown keys
The bundled schema sets additionalProperties: false at every level, and the
parser agrees: a key Lattice does not recognize fails the load before any task
is scheduled.
Error: unknown field `output` in tasks.build (lattice.json line 5, column 14)
Did you mean `outputs`?
Fields accepted here: dependsOn, inputs, outputs, ignore, env, persistent, cache, timeout
The message names the key, the object it sits in, its position in the file, the
closest accepted field, and every field accepted there. A workspace entry is
indexed, as in workspaces[1], so the message points at one entry out of
several.
There is no way to keep an extra key in the file. A lattice.json carrying a
key from an earlier release, or a note under a key of your own, has to drop it.
latticeVersion
{ "latticeVersion": "1.1.1" }
| Field | Type | Required | Default |
|---|---|---|---|
latticeVersion |
string |
no | none |
The version of Lattice this repo runs. A non-string value fails to parse. The
string itself is not validated at load time. See
Upgrading for how the version check uses this field
together with settings.versionCheck.
workspaces
{
"workspaces": [
{ "name": "web", "path": "apps/web" },
{ "name": "api", "path": "apps/api", "dependsOn": ["web"] }
]
}
| Field | Type | Required | Default |
|---|---|---|---|
workspaces |
array of workspace objects | no | [] |
Each entry is one directory with its own manifest. There is no glob form: a bare
string array such as "workspaces": ["apps/*"] fails to parse with
invalid type: string "apps/*", expected struct WorkspaceConfig. A glob key
on an entry fails as an unknown field.
The workspace object
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
yes | none | Workspace name. Unique across the file. |
path |
string |
yes | none | Literal directory path, relative to the repo root. Non-empty. |
auto |
boolean |
no | true |
With true, infer the driver and task commands from the workspace’s native manifest. With false, infer nothing and take commands from scripts alone. |
engines |
engine map | no | {} |
Per-workspace engine constraints. A key here replaces the same key at the root. |
dependsOn |
array of string |
no | none | Other workspaces, by name, this one depends on. |
scripts |
object of string to string |
no | {} |
Task name to shell command. Overrides anything inferred. Every key must name a task declared under the root tasks. |
Rejected values:
| Value | Result |
|---|---|
No name, or no path |
missing field `name` at parse time, with line and column |
path empty or whitespace-only |
workspace '<name>' has an empty path |
path absolute, or starting with a drive letter |
workspace '<name>' has a path '<path>' that is not relative to the repo root. Write every workspace path relative to the repo root |
path climbing above the repo root with .. |
workspace '<name>' has a path '<path>' that points outside the repo root. Every workspace path must stay inside the repo |
Two entries with the same name |
duplicate workspace name '<name>'. Every workspace name must be unique |
| Two entries resolving to the same directory | duplicate workspace path '<path>' in lattice.json |
path naming something that is not a directory |
workspace path '<path>' does not point to a directory. A workspace path is one literal directory, not a glob |
dependsOn naming the workspace itself |
workspace '<name>' lists itself in `dependsOn` |
dependsOn naming an undeclared workspace |
workspace '<name>' depends on '<dep>', which is not a declared workspace, plus the nearest name and the full list |
path with leading or trailing whitespace around a component |
workspace '<name>' has a path '<path>' with leading or trailing whitespace around a directory name. Windows drops that whitespace and unix keeps it, so the path would name a different directory on each. Remove it |
A scripts key that is not a declared task |
workspace '<name>' declares a script '<key>', but '<key>' is not defined in `tasks`, so nothing would ever run it, plus the nearest task name and the full list |
The same key twice in engines or scripts |
duplicate key `<key>` in workspaces[<n>].scripts, with the position |
Path checks read the string as text rather than through the host’s path rules,
so a lattice.json that is rejected on one platform is rejected on all of them.
Lattice rejects whitespace around a path component for the same reason. Windows
strips that whitespace and unix keeps it, so one string would name two
directories.
A scripts key supplies the command for the root task of the same name and
nothing else, so a key outside tasks never runs. Lattice raises an error rather
than accepting such a key, because the workspace would otherwise run its detected
command with nothing said about the override. Every key under scripts pairs
with a task of the same name:
{
"workspaces": [
{ "name": "core", "path": "libs/core", "scripts": { "build": "make all" } }
],
"tasks": { "build": {} }
}
A minimal workspace that declares its own commands:
{
"workspaces": [
{
"name": "utils",
"path": "libs/utils",
"auto": false,
"scripts": { "build": "python3 setup.py build" }
}
]
}
engines
{ "engines": { "node": ">=20.0.0", "rust": ">=1.75.0" } }
| Field | Type | Required | Default |
|---|---|---|---|
engines |
engine map | no | {} |
An engine map is a name-keyed object whose values are either a bare version
constraint string or an engine object. Root engines apply to every workspace,
and a workspace’s own entry replaces the root entry of the same name. Which
fields a value carries selects host mode, validate-only, or provisioning. See
Engines and provisioning for the three modes.
A value that is neither a string nor an object fails to parse:
Error: failed to parse lattice.json
Caused by:
invalid type: integer `123`, expected a version constraint string or an engine object at line 1 column 22
String form
{ "engines": { "node": ">=20.0.0" } }
The bare string form works only for the 40 well-known engine names, which are the names Lattice has a built-in version command for. Every built-in driver is here, plus the language toolchains those drivers sit on top of:
| Ecosystem | Names |
|---|---|
| JavaScript and TypeScript | node, deno, bun, pnpm, yarn, npm |
| Rust | rust, cargo |
| Go | go |
| Python | python, python3, pip, uv, poetry, pdm, pipenv |
| Ruby | ruby, bundler, rake |
| The JVM | java, kotlin, gradle, maven |
| .NET | dotnet, nuget |
| Swift and Objective-C | swift, pod |
| PHP | php, composer |
| Elixir | elixir, mix |
| Dart | dart |
| Haskell | haskell, ghc, stack, cabal |
| Task runners | just, task, turbo, nx |
Each name’s version command is on Toolchains.
A string-form engine outside that list fails validation, before any task runs:
Error: engine 'alpes' in root uses the string (version-only) form, but 'alpes' is not a well-known engine Lattice can version-check on its own. Use the object form with an explicit `versionCmd`, e.g. "alpes": { "version": ">=1.0.0", "versionCmd": "alpes --version" }
Object form
{
"engines": {
"alpes": {
"version": ">=2.6.7",
"versionCmd": "alp --version",
"installCmd": "curl https://example.com/alp.sh | sh",
"bin": "bin"
}
}
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
version |
string |
no | none | Version constraint, such as ">=13.0.0". A loose value such as "1.22" is read as a lower bound. |
versionCmd |
string |
no | none | Command that prints the tool’s version. Required for any name outside the well-known list. |
installCmd |
string |
no | none | Command that installs the toolchain. Its presence selects provisioning mode. It receives $LATTICE_TOOLCHAIN_DIR both as an environment variable and by literal substitution into the command string. |
bin |
string |
no | "bin" |
Bin directory inside the toolchain install, prepended to the task’s PATH. Read in provisioning mode only. Must be relative, non-empty, and stay inside the install. |
The object form accepts any engine name. A name outside the well-known list that
carries a version but no versionCmd parses, then fails when Lattice needs
the version:
engine 'alpes' has a version constraint but no way to check it (not a well-known engine and no `versionCmd`)
Lattice joins bin to the toolchain’s install directory and puts the result at
the front of the PATH of every task that resolves the engine. The check
therefore runs when the config loads: bin has to be relative, non-empty, free
of whitespace around any component, and inside the install directory. "bin",
".", "usr/local/bin", and "bin/../libexec" all pass. "/usr/bin" and
"../../.." are rejected. Either one would put a directory Lattice never
provisioned in front of every command, while the run still reported a provisioned
toolchain:
engine 'node' in root has a `bin` of '/usr/bin', which is not relative to the toolchain install. Write `bin` as a path inside the toolchain directory, like "bin"
globalDependencies
{
"globalDependencies": ["tsconfig.base.json", "proto/**", ".env"]
}
| Field | Type | Required | Default |
|---|---|---|---|
globalDependencies |
array of string |
no | [] |
Globs relative to the repo root, hashed into the cache key of every task. A
task’s inputs are relative to its own workspace, so a file above the workspace
cannot be named there in a way that means the same thing everywhere. Editing
anything matched here makes every task in the repo miss. Lattice hashes these
patterns once at the start of a run, so a malformed pattern fails the whole run
rather than one task. See Cache internals.
lattice init writes this key when a workspace’s turbo.json declares
globalDependencies. Prune what it brought over on the rule above: a pattern
that does not really cross workspaces costs every task a miss.
globalEnv
{
"globalEnv": ["NODE_ENV", "CI"]
}
| Field | Type | Required | Default |
|---|---|---|---|
globalEnv |
array of string |
no | [] |
Environment variable names whose resolved values feed the cache key of every
task. A name with no value is hashed as declared-and-unset, which is a different
key from not listing the name at all. Unlike a task’s env, these names are not
set on task processes: they are already in the environment Lattice inherited. A
task’s own env list applies on top of this one. See Environment
variables.
lattice init writes this key too, when a workspace’s turbo.json declares
globalEnv.
tasks
{
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**/*", "package.json"],
"outputs": ["dist/**"]
}
}
}
| Field | Type | Required | Default |
|---|---|---|---|
tasks |
object of string to task object |
no | {} |
Keys are task names you choose. Declaration order is preserved and does not affect execution order, which comes from the dependency graph. Passing a name that is not a key here fails immediately:
Error: task 'build' is not defined in the `tasks` map in lattice.json. Defined tasks: test
With no tasks declared at all, the same error ends with
lattice.json defines no tasks.
The task object
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
dependsOn |
array of string |
no | none | Task dependencies. ^task means that task in this task’s dependency workspaces. A bare task means that task in the same workspace. |
inputs |
array of string |
no | the whole workspace | Globs for the files whose contents feed the cache key. Omitted, every file in the workspace is hashed except what .gitignore excludes and what this task’s own outputs match. |
outputs |
array of string |
no | none | Globs for the files captured as the cached artifact. The files these match are excluded from input hashing. The pattern strings themselves are part of the cache key. |
ignore |
array of string |
no | none | Globs excluded from input hashing. |
env |
array of string |
no | none | Environment variable names whose resolved values feed the cache key. Lattice also sets these on the task’s process. |
persistent |
boolean |
no | false |
A task not expected to exit, such as a dev server. Never cached, whatever cache says. See Persistent tasks. |
cache |
boolean |
no | true |
Set false to opt a non-persistent task out of caching. |
timeout |
string or integer |
no | none | How long the task may run before Lattice stops it and counts it as failed. Maximum 365 days. Ignored on a persistent task. |
timeout accepts ms, s, sec, secs, m, min, mins, h, hr, and
hrs, case-insensitive, or a bare whole number of seconds. A string value below
one second rounds up to one second. An unrecognized unit fails to parse:
Error: failed to parse lattice.json
Caused by:
unknown duration unit 'fortnights' in '5 fortnights' at line 1 column 43
The maximum is 365 days, 31536000 seconds, in both forms. Lattice rejects an
oversized value rather than clamping it. Past 365 days the deadline overflows
instead of arriving, so an oversized timeout would mean no timeout at all:
duration '99999h' is longer than the maximum of 365 days. Use a shorter duration, or leave `timeout` out to let the task run without a limit
The number form is an integer, the way the bundled JSON schema has always
declared it. Lattice rejects 1.5 rather than rounding it up to 2. The string
form takes decimals, so "1.5m" and "90s" both parse. 90.0 counts as a whole
number of seconds:
duration 1.5 is not a whole number of seconds. Write a whole number of seconds, or a duration string such as "90s", "1500ms", or "10m"
A glob in inputs, outputs, ignore, or globalDependencies is compiled
when the task’s cache key is computed rather than when the config loads, so a
malformed pattern fails that one task:
a:build: failed to compute cache key: error parsing glob 'src/[[': unclosed character class; missing ']'
A dependsOn entry naming a task the map does not define fails validation, and
so does a task that names itself:
Error: task 'build' depends on 'tset', but 'tset' is not defined in `tasks`
Defined tasks: build, test
Error: task 'build' lists itself in `dependsOn`
An empty task object is valid. It declares no dependencies, no inputs, and no outputs, and it still caches: its key covers the whole workspace, so it re-runs whenever anything in that directory changes.
{
"tasks": {
"dev": { "persistent": true, "cache": false },
"clean": {}
}
}
settings
{
"settings": {
"maxCacheSize": "10GB",
"cacheDir": ".lattice/cache",
"loquacious": false,
"versionCheck": true
}
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
maxCacheSize |
string |
no | none | Upper bound on the local cache size. Enforced after every run, and used by lattice prune when --max-size is absent. Unset, the cache grows without limit. |
cacheDir |
string |
no | ".lattice/cache" |
Directory for the local cache, relative to the repo root. Must be non-empty, stay inside the repo, and not be the repo root itself. |
loquacious |
boolean |
no | false |
With true, always use raw output, as -v/--verbose does. |
versionCheck |
boolean |
no | true |
With true, compare the running binary against latticeVersion. With false, skip the check. See Upgrading. |
maxCacheSize is a string: an integer or decimal followed by B, KB, MB,
GB, or TB, base 1024 and case-insensitive. K, M, G, and T are
accepted as short forms, and a string of digits with no unit is read as bytes.
{ "settings": { "maxCacheSize": "512MB" } }
A JSON number fails to parse, even though the same digits work as a string:
Error: failed to parse lattice.json
Caused by:
invalid type: integer `536870912`, expected a string at line 1 column 41
An unrecognized unit fails the same way:
Error: failed to parse lattice.json
Caused by:
unknown cache size unit 'QB' in '512QB' at line 1 column 36
With settings.maxCacheSize unset, lattice prune needs --max-size:
Error: no cache size limit set. Pass --max-size, or set settings.maxCacheSize in lattice.json
Lattice validates cacheDir when the config loads, and owns the directory
cacheDir names: lattice prune deletes the *.tar.gz, *.meta.json, and
*.tmp files it finds there. The value therefore has to be relative, has to stay
inside the repo, and cannot resolve to the repo root. ".lattice/cache" and
".cache/lattice" both pass. "." does not, because it would point prune at
the top of the repo:
Error: `settings.cacheDir` is '.', which is the repo root itself. Point it at a directory of its own, like ".lattice/cache" — `lattice prune` deletes cache archives and partial writes in whatever directory it names
"/tmp/lattice" and "../cache" are rejected as well, each with its own
message. See Errors for all five.
When each check runs
| Problem | Stage | Error |
|---|---|---|
No lattice.json in this or any parent directory |
before parsing | no lattice.json found in this directory or any parent. Run `lattice init` to create one |
| Malformed JSON | parsing | failed to parse lattice.json, with the underlying JSON error and its position |
A workspace object missing name or path |
parsing | missing field `name` , with line and column |
| A key Lattice does not recognize, at any level | parsing | unknown field `<key>` in <path>, with position, the nearest valid field, and the fields accepted there |
| A value of the wrong JSON type | parsing | invalid type: <what was written>, expected <what the field takes> |
| An engine value that is neither a string nor an object | parsing | invalid type: <what was written>, expected a version constraint string or an engine object |
An unrecognized timeout or maxCacheSize unit |
parsing | unknown duration unit '<unit>' in '<value>', unknown cache size unit '<unit>' in '<value>' |
A timeout over 365 days, or a fractional number of seconds |
parsing | duration '<value>' is longer than the maximum of 365 days, duration of <n> seconds is longer than the maximum of 365 days, duration <n> is not a whole number of seconds |
The same key twice in tasks, engines, or a workspace’s engines or scripts |
parsing | duplicate key `<key>` in <container>, with the position |
| A string-form engine whose name is not well-known | validation | names the engine and suggests the object form with versionCmd |
A workspace path that is empty or whitespace-only |
validation | workspace '<name>' has an empty path |
A workspace path that is absolute or climbs above the repo root |
validation | ... is not relative to the repo root, ... points outside the repo root |
A workspace path with whitespace around a component |
validation | ... with leading or trailing whitespace around a directory name |
A settings.cacheDir that is empty, absolute, outside the repo, or the repo root |
validation | names the field, quotes the value, and says which rule it broke |
An engine bin that is empty, absolute, or outside the toolchain install |
validation | engine '<name>' in <scope> has a `bin` of '<value>', which ... |
A workspace scripts key that names no declared task |
validation | workspace '<name>' declares a script '<key>', but '<key>' is not defined in `tasks` , with the nearest name and the full list |
Two workspaces with the same name |
validation | duplicate workspace name '<name>'. Every workspace name must be unique |
A workspace or task that lists itself in dependsOn |
validation | workspace '<name>' lists itself in `dependsOn` , task '<name>' lists itself in `dependsOn` |
A workspace dependsOn naming an undeclared workspace |
validation | workspace '<name>' depends on '<dep>', which is not a declared workspace, with the nearest name and the full list |
A task dependsOn naming a task not in tasks |
validation | task '<name>' depends on '<dep>', but '<dep>' is not defined in `tasks` , with the nearest name and the full list |
A workspace path that is not a directory |
workspace discovery | workspace path '<path>' does not point to a directory. A workspace path is one literal directory, not a glob |
| Two workspaces resolving to one directory | workspace discovery | duplicate workspace path '<path>' in lattice.json |
An auto workspace with no unambiguous driver |
workspace discovery | workspace '<name>' has an ambiguous or undeclared driver., with candidates and a fix |
A task name passed to lattice run that is not in tasks |
after config loads | task '<name>' is not defined in the `tasks` map in lattice.json, with the defined task names |
lattice prune with no size limit anywhere |
after config loads | no cache size limit set. Pass --max-size, or set settings.maxCacheSize in lattice.json |
| An engine that fails its version constraint | before any task runs | engine '<name>' on PATH is <version>, which does not satisfy the constraint '<constraint>' |
A malformed glob in inputs, outputs, ignore, or globalDependencies |
while running | failed to compute cache key: error parsing glob ... |
Everything at the parsing and validation stages is raised before Lattice looks at a single workspace directory, so nothing has run when you see it.
Complete example
A repo with a Node app, a Rust service, and a Python library:
{
"$schema": ".lattice/schema.json",
"latticeVersion": "1.1.1",
"workspaces": [
{ "name": "web", "path": "apps/web", "engines": { "node": ">=20.0.0" } },
{
"name": "api",
"path": "services/api",
"dependsOn": ["shared"]
},
{
"name": "shared",
"path": "libs/shared",
"auto": false,
"scripts": {
"build": "python3 -m build",
"test": "python3 -m pytest",
"lint": "python3 -m ruff check ."
}
}
],
"engines": {
"rust": ">=1.75.0",
"protoc": {
"version": ">=25.0.0",
"versionCmd": "protoc --version",
"installCmd": "curl -fsSL https://example.com/protoc.sh | sh -s -- \"$LATTICE_TOOLCHAIN_DIR\"",
"bin": "bin"
}
},
"globalDependencies": ["tsconfig.base.json", "proto/**"],
"globalEnv": ["NODE_ENV"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**/*", "Cargo.toml", "package.json"],
"ignore": ["**/*.test.*"],
"outputs": ["dist/**", "target/release/**"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*", "tests/**/*"],
"env": ["DATABASE_URL"],
"timeout": "10m"
},
"lint": {
"inputs": ["src/**/*"]
},
"dev": {
"persistent": true,
"cache": false
},
"clean": {}
},
"settings": {
"maxCacheSize": "10GB",
"versionCheck": true
}
}