Skip to content
Foundry
Esc
↑↓navigate↵open⌘Jpreview
On this page

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>'.

Was this page helpful?