Configuring a Beach Application
A Beach application is described by a small tree of YAML files. The loader reads them at startup, applies a per-collection cascade, and hands the application a frozen, typed object. Adopters who prefer to construct the primitives directly from TypeScript can still do so — the YAML route is what the documentation recommends, because Beach's brand promise is that the integration surface diffs in code review, not in the orchestrator's setup file.
The three principles, in order:
- Config is data. YAML on disk, validated by JSON Schema. Other languages read the same files.
- Three orthogonal axes. Functional (what the application is), environmental (where it runs and how it connects), secret (credentials only). Each axis has its own files, schemas, and review path.
- Defaults everywhere. Most fields take their value from a per-model or per-kind cascade, so a 200-actor application's per-actor files stay short.
The default scaffold
beach-config init lays down this layout:
config/
beach.yaml # umbrella: pulls everything in
beach.functional.yaml # behavioural defaults
beach.environment.yaml # operational defaults
beach.environment.<env>.yaml # per-env overlays (optional)
beach.secrets.env # credentials only (gitignored)
models/ # one file per model class
haiku.yaml
sonnet.yaml
opus.yaml
actors/ # one file per actor
channels/ # one file per channel
tools/ # one file per tool
The directory tree is the application's table of contents. Open it,
read the filenames, and you have the inventory: which models the
application uses, which actors run, which channels it reaches, which
tools it exposes. A beach-config tree walk reads the same data and
prints it as one navigable view.
The three axes
| Axis | Files | Reviewer |
|---|---|---|
| Functional | beach.functional.yaml, files under actors/, tools/, models/ |
App engineers, code review |
| Environmental | beach.environment.yaml, beach.environment.<env>.yaml, files under channels/ |
Ops, deployment review |
| Secret | beach.secrets.env |
Secrets owner, rotation gate |
The functional axis describes what the application is. A change here is a feature change: a new actor, a new tool, a different model, a different prompt. It diffs through the same review process as the orchestrator's code, because changing it changes what the application does.
The environmental axis describes where the application runs. A change here is an operational change: a new Redis URL for production, a faster poll interval for staging, a stricter log level for prod. It diffs through deployment review.
The secret axis carries credentials only. Anything that is not a credential — a hostname, a port, a URL without embedded credentials, a model name, a feature toggle — belongs in one of the other two axes. The validator flags the leakage explicitly.
${VAR} references are permitted in environmental files (and in the
per-environment overlays) for credential-bearing fields. The loader
resolves them against beach.secrets.env first, then process.env,
and throws if a reference resolves to nothing. References in the
functional axis are rejected at validation time; the message points
the operator at the correct file.
Where does this go?
Operators and engineers running into "where does this setting belong?" should consult the table:
| Setting | File | Diffed by |
|---|---|---|
| Orchestrator prompt | actors/<id>.yaml |
App engineers, code review |
| Tool list for an actor | actors/<id>.yaml |
App engineers, code review |
domainDataSchema for a triage actor |
actors/<id>.yaml |
App engineers, code review |
| Cumulative token budget strategy | beach.functional.yaml |
App engineers, code review |
| Default head strategy | beach.functional.yaml |
App engineers, code review |
| Cascade-suppression behaviour | beach.functional.yaml |
App engineers, code review |
| Whether MCP / A2A / REST is enabled | beach.functional.yaml |
App engineers, code review |
| Redis URL | beach.environment.yaml |
Ops, deployment review |
| IMAP host and port | beach.environment.yaml |
Ops, deployment review |
| Log level | beach.environment.yaml |
Ops, deployment review |
| Poll interval | beach.environment.yaml |
Ops, deployment review |
| OTLP endpoint | beach.environment.yaml |
Ops, deployment review |
ANTHROPIC_API_KEY |
beach.secrets.env |
Secrets owner, rotation gate |
IMAP_PASS, SMTP_PASS |
beach.secrets.env |
Secrets owner, rotation gate |
The table is the single most useful artefact in the configuration documentation. A new contributor reads it and answers "where does this go?" without further reasoning.
The cascade
Every collection — actors, channels, tools, models — follows the same three-layer cascade: instance → perKind/perModel → defaults.
# beach.functional.yaml
beach:
version: 1
actors:
defaults:
maxTokens: 4096
temperature: 0.7
perModel:
claude-haiku-4-5:
maxTokens: 1024
temperature: 0
inject: { maxInjectTokens: 50000 }
claude-sonnet-4-6:
maxTokens: 8192
inject: { maxInjectTokens: 200000 }
A kind: llm actor whose model: is claude-haiku-4-5 inherits
maxTokens: 1024 from the perModel layer; an actor whose model is
claude-sonnet-4-6 inherits maxTokens: 8192. Either can override
the field on its own file. The cascade closes over what the actor file
does not declare.
Tools and channels use perKind instead of perModel. The shape is
otherwise identical.
The cascade is what keeps a 200-actor application's per-actor files short. A typical actor file is three or four fields — model, prompt, tools — and inherits everything else.
Inline or split
Two layouts, equivalent at the loader level. Pick the one that fits the application's size:
Split (default). One file per actor, channel, tool, model. The
filesystem tree carries the inventory. beach-config init writes
this layout.
Inline. Everything in beach.functional.yaml's inline: blocks.
Fine for prototypes; gets unwieldy past five or six entries per
section.
my.cnf-style include directives mix and match: a team can keep
common actors in actors/, declare a few specialist actors inline,
and pull in a third group from actors/specialists/ through an
includeGlob directive. The loader assembles them all into one
inventory.
beach-config split <section> and beach-config merge <section>
move a section between the two layouts without changing anything
the application sees.
Models
Three model files ship with the scaffold: haiku.yaml, sonnet.yaml,
opus.yaml. Each declares what Beach uses to drive its behaviour —
context window, default maxTokens and temperature, the inject budget —
and the matches: array of model versions the file applies to.
# config/models/haiku.yaml
familyAlias: haiku
matches:
- claude-haiku-4-5
contextWindow: 200000
maxTokens: 1024
temperature: 0
inject:
maxInjectTokens: 50000
headStrategy: first-n
The model files are user-editable; beach-config update-models is
the opt-in command that refreshes them when Anthropic ships a new
version. The default refresh strategy appends new entries to
matches[] and leaves the team's other customisations alone.
Beach maintains no cost or pricing data. A team that wants cost reporting writes its own analyser; the file format is stable.
Prompts
The actor schema accepts three forms of prompt declaration:
systemPromptInline:— inline in YAML. For prompts a team is happy editing as a YAML scalar.systemPromptFile:— a path to a markdown file. For prompts maintained outside YAML by a different toolchain.- Neither field — the application supplies the prompt at construction time. For applications that compose prompts from data, fragments, templates, or anything else.
Beach validates that one of the three resolves to a string by the time
runTurn is called, and is silent on prompt management beyond that.
No directory imposed, no format imposed.
Application-side functional config
Beach is silent on the shape of an application's own functional config, but the config tree is the right place to keep it. Three reserved namespaces give consumers room without forcing Beach to validate anything it has no opinion about:
- Top-level
app:on the functional file — application-wide settings. The application's startup code readsconfig.functional.app.<your-field>. - Per-instance
app:on actor / channel / tool files — settings scoped to that one thing (a per-channel A2UI catalogue list, a per-actor system-prompt fragment, a per-tool budget). app.collections.<name>on the functional file — consumer-defined collection-shaped data using the sameinline | include | includeGlob | includeDir | includeOptionalvocabulary Beach's own collections use. The loader walks the named directories and surfaces the assembled instances atconfig.functional.app.collections.<name>.instances.
Recommendation: prefer app.collections.<name> for any list of
similar-shaped things — branding profiles, feature flag sets,
catalogue declarations, registry entries. Why: every adopter who
hand-rolls a parallel config tree alongside Beach's loses the same
loader, the same beach-config tree view, and the same auditability
that Beach's own collections enjoy.
# beach.functional.yaml — a branding-collection example
beach:
version: 1
app:
collections:
branding:
includeDir: [branding/]
# branding/light.yaml — application-shaped, contents governed by the
# application's own JSON Schema (passed to --with-app-schema).
primary: "#0066cc"
background: white
Beach validates the shape of each entry (it is an object) and is
otherwise silent. To bring app: blocks under per-file checking,
ship a JSON Schema with the application and run:
beach-config validate --with-app-schema ./schemas/app.schema.json
Recommendation: ship one of these with every Beach application that
has any consumer-defined app: content. Why: it brings consumer
fields under the same per-file checking Beach gives its own fields,
and a single command answers "is this config tree internally
consistent?" across both axes.
YAML or JSON
Beach reads both. The scaffold writes YAML; per-thing files default
to YAML; the convention walk picks up .yaml, .yml, and .json
indifferently.
Recommendation: hand-edited config files use YAML. Why: YAML supports comments and tolerates trailing whitespace, which humans rely on when reasoning about per-thing files.
Recommendation: machine-edited config files use JSON. Why: JSON has a single canonical serialiser and round-trips cleanly through tooling without having to preserve comments. An admin page in the application that lets an operator edit a feature-flag set or a branding palette is the natural case — the page reads the file, mutates the object, and writes JSON back without worrying about indent levels or string folding.
A directory may mix YAML and JSON during a migration; Beach treats both formats as equivalent at the loader level. Recommendation nonetheless: settle on one format per directory once the migration is complete, because mixed directories are harder for humans to scan at a glance.
What ships in v1, what does not
In v1:
- The umbrella loader, the validator, and the four CLI inspection
commands (
init,validate,tree,explain). - The
split/mergeround-trip between layouts. - The
update-modelsrefresher. fromConfigfactories onEventRouter,ManifestRegistry, andSessionTurnManager.
Out of v1:
- Cost reporting, pricing data,
--costflags. The shipped model files carry no cost fields. - Automatic model deprecation. Old versions stay in
matches[]until someone removes them. - Remote includes (HTTPS URLs, OCI artefacts). Local files only.
- A runtime config service. Files on disk, read once at startup.
Migration from direct construction
Existing applications that construct primitives directly continue to
work — EventRouter.fromConfig and friends are additive. The
documentation recommends loadBeachConfig-driven construction for new
applications, because it is what makes the integration surface
auditable in code review. Existing applications can migrate at their
own pace, one primitive at a time.
Related
- Reference: configuration schemas — the JSON Schemas in detail.
- Reference: beach-config CLI — every subcommand.
- Getting Started — install Beach and run the canonical pipeline.