Project structure and environment
Folder discovery, ignored files, workspace roots, setup, names, and module resolution.
Marbles loads modules from .foundry/marbles/ under the current working
directory. There is no required entrypoint or configuration object. Calling
step, workflow, schedule, or monitor registers definitions and triggers
as modules load. Exports are optional and useful for sharing definitions.
Discovery is owned by the CLI; file operations inside a step use Workspaces. The resolved project path also scopes local state and declared artifact identity. Keep it stable to retain access to the same saved runs and artifacts. See Durability.
Discovery and ignored files
Discovery walks subdirectories and imports JavaScript and TypeScript modules
in sorted path order: .ts, .tsx, .mts, .cts, .js, .jsx, .mjs, and
.cjs. A module also imported by another module executes only once per process.
Names must be unique across the whole catalog.
A leading underscore excludes a file or directory from automatic discovery.
_helpers.ts is skipped; _dashboard/ and everything inside it are skipped.
A trailing underscore has no special meaning. Ignored code can still be
imported normally, including any registrations or side effects it performs.
The convention controls discovery, not permissions or execution isolation.
Hidden entries, node_modules, TypeScript declaration files, and symlink
entries are also skipped. Other files, such as Markdown or images, are not
imported automatically. Put tests and micro apps under underscore directories
when their source must not load at startup.
.foundry/marbles/
review.ts automatically loaded
releases/watch.ts automatically loaded
_shared/agents.ts imported by other modules when needed
_apps/dashboard/server.ts started explicitly by a step when needed
_test/review.test.ts loaded by your test runner
Keep top-level code to declarations. Start servers and other work in step bodies, rather than during module import. Discovery is a startup operation; restart Marbles after changing modules.
Setup
Run marbles init from the project root. It writes one declaration under
.foundry/marbles/, creates parent directories, and refuses to overwrite an
existing file. It does not execute the step.
| Starter | Created file |
|---|---|
product, the default |
summarize-codebase.ts |
developer |
code-review.ts |
design |
prototype.ts |
Use marbles init <starter> to select one, including in an existing folder.
If the authoring folder is absent, interactive startup offers the same Developer, Design, and Product starters with editable instructions, name, and harness selection. It writes only when Create is activated. Skipping setup opens an empty dashboard without creating the folder. An existing empty folder registers nothing and skips setup. Piped commands use an empty catalog when no source exists.
The workspace
With .foundry/marbles, the workspace is the project directory above
.foundry, not the authoring directory. Agent sessions, sandbox workspaces,
workspaces.current, relative workspace paths, and report media use that
project root. State lives under ~/.foundry/marbles/<workspace-id>.
--source <path> selects another directory or a single module. A custom
directory is itself the workspace; a single module uses its containing
directory. Selecting .foundry/marbles or a module inside it uses
the project root even when invoked from another working directory.
--config remains an alias for --source. When the default folder is absent,
an existing ./marbles.config.ts still loads for compatibility. The folder
takes precedence when both exist. With no source, the working directory is
the workspace. help and init do not import authoring modules.
Environment
Declare tokens and settings in a Varlock .env.schema
at the workspace root. For example, on macOS this schema resolves a token
from the Keychain:
# .env.schema
# @defaultRequired=true
# ---
# @sensitive
FIGMA_TOKEN=exec(`security find-generic-password -s figma -w`)
run, roll, and list load the schema before importing authoring modules,
using the workspace’s own varlock or one on PATH. Resolved values are
added to the environment without replacing values already set. Marbles
resolves secrets each time a process starts and does not persist the loaded
values. Without a schema, Marbles leaves the environment as it is.
roll stops when a required value does not resolve. The dashboard, headless
run, and list print what is missing and continue; work that needs the
missing value can still fail. help, init, status, and sessions do not
load the schema.
Names and the catalog
A named step or workflow is a marble, and together they are the catalog:
list prints it, roll <name> runs one, the dashboard launches one from its
Marbles panel, and a schedule or monitor can target one.
Name launchable work explicitly with step("review") or workflow("review", tree).
Inferred names
A definition built without a name takes the top-level const it is assigned to,
read from the module’s source with the project’s own typescript, which is
an optional peer resolved from the module’s location. Without typescript,
or when the definition is made inside a function or inline, it stays
internal: usable by reference, absent from list, and refused as a trigger
target with an error that says to name it.
A repeated name fails when the module loads. The error names the position of
each registration and says when one came from a const. Schedules and
monitors have no names; they are keyed by what they trigger and watch. See
triggers.
Definitions such as agent, workspace, sandbox, artifact, and skill
sets are not catalog entries. Only steps and workflows are launched.
Package resolution
The CLI supplies @foundry.sh/marbles and @foundry.sh/marbles/prebuilt to the
authoring module itself, through a Bun loader plugin, so the modules and the CLI
share one registry. Any other library the module imports, such as zod, must
resolve relative to the importing file in the usual way. Install it in that
project.
For CLI installation and editor types, see Installation.
A plain script importing the package can use instance methods and .run();
call dispose() when finished. A custom host can construct an Engine and
register definitions explicitly; see Embedding the engine.
Prebuilt steps
Import a factory from @foundry.sh/marbles/prebuilt and call it to register the
one step you want. Importing the module alone registers nothing.
import { summarizeCodebase } from "@foundry.sh/marbles/prebuilt";
summarizeCodebase({ name: "summarize-codebase" });
Each factory opens a read-only reviewing agent through the context, so the
working directory, cancellation, and streaming are already wired. Options are
name, extra instructions appended to the read-only prompt, provider
(codex, claude-code, or pi; omitted means the first installed CLI), and
timeoutMs for the turn, two minutes by default, with no retries.
| Factory | Input | Result |
|---|---|---|
summarizeCodebase(options?) |
none | { provider, summary }, an orientation for a product teammate |
codeReview(options?) |
none | { provider, text }, at most five actionable findings |
prototype(options?) |
{ brief } |
{ provider, text }, self-contained HTML/CSS in the reply; nothing is written to disk |
promptStep(name, prompt, options?, description?) |
none | { provider, text } for a prompt of your own |
reviewInWorktree(options?) |
{ repository, base? } |
A review of base (default HEAD) checked out in a temporary worktree of repository |
reviewSession(options?) |
{ sessionId? } |
A review that continues the same conversation on every run (default id code-review) |
Default names use the factory name in kebab case, such as code-review. Composed examples
live in apps/marbles/examples/ in the repository; load one with --source
or copy from it. They are not registered by the CLI.
The launch form
Select a row in Marbles and press l. A step without .input(schema) launches
at once. A step with one gets a form derived from the schema’s JSON Schema:
a flat object of strings, numbers, booleans, and string enums becomes text,
multiline, number, boolean, and select fields, with labels, descriptions,
required flags, defaults, and numeric bounds carried over. .describe(text)
on the builder is shown above the form. Any other shape, or a schema library
without JSON Schema output, becomes one JSON field.
The factory parses the input at dispatch, once. A value the schema rejects
keeps the form open and shows the error; nothing is registered in the run
table. On acceptance the dashboard selects the new run and clears run
filters. Schedules and monitors keep running. Tab moves focus, Ctrl+Enter
submits, Esc returns without launching. Nested input that the form cannot
express is reachable with marbles roll <name> --input '<json>'.