Project Structure¶
The only rule¶
Mycel reads every file ending in .mycel, recursively, from one
configuration directory, and merges them into a single service definition.
That is the whole loading model. There is no manifest, no include list, no entry
point file. If a .mycel file is under the config directory, it is part of your
service; if it is not, it is invisible to Mycel no matter what it contains.
mycel start --config ./my-service # explicit
mycel start # defaults to the current directory
docker run -v $(pwd):/etc/mycel mdenda/mycel # the image reads /etc/mycel
Two consequences worth internalising:
- Files are merged, not scoped. A connector declared in
connectors/db.mycelis visible to a flow inflows/orders/create.mycel. Splitting files is purely for your benefit as a reader. - Directory and file names mean nothing to the runtime.
connectors/,flows/,aspects/are conventions this documentation recommends because they help humans, not because Mycel looks for them. Putting everything in oneservice.mycelproduces an identical service.
Recommended layout¶
Start with one file and split when a file stops fitting on a screen. These are the three shapes that show up in practice.
Fine for a demo, a proof of concept, or a service with one or two flows.
The split most services stay at. One file per kind.
One file per thing, grouped by kind. This is the layout used by the production consumers Mycel was built against.
my-service/
config.mycel
connectors/
rabbit.mycel
magento_db.mycel
slack.mycel
flows/
style_create.mycel
style_update.mycel
aspects/
slack_notifier.mycel
slack_error_notifier.mycel
shared/
order_lock.mycel # named reusable blocks
Past a dozen flows, group them by domain rather than by kind —
flows/orders/, flows/inventory/. Nesting depth is unlimited and costs
nothing.
The guideline that matters: name the file after what it declares. When a
flow misbehaves, you want flows/style_update.mycel to be an obvious place to
look, and a file called retry.mycel holding flow "create_order" defeats
that.
mycel validate mentions it when a file declaring exactly one thing is named
after something else:
- delete_customer.mycel declares only flow "create_user" — renaming it
create_user.mycel would say so
A file holding several declarations is exempt: it is a collection, and
flows.mycel is an honest name for one. Names are scoped by kind, so a lock
and a sequence_guard may both be called collection; qualifying the file as
collection_lock.mycel is correct and accepted, since they cannot share
collection.mycel.
Names are global¶
Because every file is merged, names must be unique across the whole
directory, not per file. Two connectors called db in two different files is
an error, caught at parse time:
This applies to connectors, flows, types, transforms, aspects and every kind of named reusable block. It is the one way file organisation can bite you, and it fails loudly rather than silently picking one.
Files Mycel does not read¶
| Path | What happens |
|---|---|
Anything not ending in .mycel |
Ignored, with the exceptions below |
mycel_plugins/ |
Skipped entirely — this is Mycel's own plugin cache, not your config |
A .mycel plugin manifest |
Skipped by the config parser and read by the plugin loader instead. Identified by content, not name: a top-level plugin { } block with no label plus a provides { } block. A plugin declaration in your config — plugin "name" { source = "..." }, with a label — is ordinary config and is read normally |
So a README.md, a .sql file or a shell script can live beside your config
without any effect. Two non-.mycel file types are meaningful, but only
because other subsystems look for them at fixed paths:
.env— read from<config-dir>/.env, falling back to./.env. Loaded before parsing soenv()resolves. Never overrides variables already set in the environment.-
Mock fixtures — JSON, at paths that are structural, since the mock loader resolves them by name rather than scanning:
Path Serves mocks/connectors/{connector}/{target}.jsonA read against that target mocks/connectors/{connector}/{METHOD}_{path}.jsonAn HTTP call, e.g. GET_users.jsonmocks/flows/{flow_name}.jsonA whole flow's output These are the only paths where a directory name is load-bearing.
Per-environment configuration¶
There is no per-environment directory. Running the same config against
development and production is done in two ways, both inside your .mycel files:
env()for anything that differs by value — hosts, credentials, ports. Give it a default where one makes sense:env("DB_HOST", "localhost").- Connector profiles for anything that differs by shape — a connector that is a local database in development and a remote API in production.
MYCEL_ENV selects environment-aware defaults
(log level, log format, and similar) and can drive profile selection. It does
not load a different set of files.
Checking your structure¶
mycel validate parses the whole directory and reports what it found. Run it
before anything else — it needs no connections and no deployment environment:
✓ Configuration is valid!
Connectors: 3
- rabbit (mq)
- magento_db (database)
- slack (notification)
Flows: 2
- style_create: * → styles
- style_update: * → styles
Types: 0
If a connector or flow you expected is missing from that list, the file
declaring it is not being read — check the extension first, then that it is
actually under --config.
Once a single file passes eight declarations, validate also mentions it:
○ Readability (nothing is wrong):
- everything.mycel declares 10 things — consider splitting into connectors/ and flows/
Mycel merges every .mycel file, so this changes nothing at runtime.
It is advice, never a failure, and it appears only here — not in mycel start.
Where a declaration lives changes nothing at runtime, so a running service has
no business raising it, and a startup that warns about style is one whose real
warnings get ignored.