The canonical on-disk layout of an IOModel project: one tree for Studio, a local folder, and git.
An IOModel project is one file tree. The same paths appear in a local (file) project, in a git repository, and in a cloud project’s commit manifest — and that is the tree Studio shows. The layout is the source of truth: the cloud does not store paths that cannot exist on disk.
One layout, three places
Export a cloud project or import a git repository and you get the same
document paths. Navigation files (meta.yaml) are written in Studio’s
canonical form — one URL segment per pages: entry, with nested folders
holding their own meta.yaml.
Project root
<project>/
├── project.yaml # title, description, icon, features
├── models.yaml # model index
├── tags.yaml # optional global tag taxonomy
├── models/ # models, objects, workflows, declared attachments
├── specs/ # documentation
└── schema/ # optional custom object schemas & templates| File | Purpose |
|---|---|
project.yaml | Project-level title and settings. |
models.yaml | Top-level index that registers each model by key (models: { <key>: null }). |
tags.yaml | Global group:value tag taxonomy (optional; can also live inline in models.yaml). |
Models
A model describes one product, system, or domain. Models are registered in models.yaml and split into per-object / per-workflow files for clean diffs.
models.yaml # models: { deployment: null }
models/
└── deployment.yaml # model root: metadata + objects/workflows declarations
│
├── objects/ # object tree (one file per object)
│ ├── iomodel.yaml # object "iomodel"
│ └── iomodel/ # folder = children + attachments of "iomodel"
│ └── …
│
└── workflows/ # workflow tree (one file per workflow/group)
├── sign-in.yaml # workflow "sign-in" (type: wf)
├── sign-in/ # folder = attachments of "sign-in"
│ └── …
├── billing.yaml # group "billing" (type: group)
└── billing/
└── checkout.yaml # nested workflow "billing.checkout"Model index — models.yaml
models:
deployment: # value is null; the body lives in models/deployment.yamlModel root — models/<model>.yaml
Holds model metadata plus declarations of the first-level objects and workflows. Declarations are maps of key: null; each declared key points at a sibling file under objects/ or workflows/.
title: Deployment Environments
type: model
objects_schema: kubernetes # object vocabulary (c4, kubernetes, custom id, or URL)
objects:
iomodel: null # → models/deployment/objects/iomodel.yaml
workflows:
sign-in: null # → models/deployment/workflows/sign-in.yaml
release-upgrade: nullStrict declarations
A file under objects/ or workflows/ is only loaded if its key is declared
in the parent (objects: / workflows:). An undeclared file is ignored —
there is no implicit pickup. This keeps the tree explicit and ordered
(declaration order = display order).
Objects — models/<model>/objects/**
Each object is one YAML file; its folder of the same name holds its children and attachments.
models/<model>/objects/<slug>.yaml # the object definition (declares its own children)
models/<model>/objects/<slug>/<child>.yaml # nested child object# models/deployment/objects/iomodel.yaml
title: IOModel
type: deployment
objects:
api: null # → models/deployment/objects/iomodel/api.yamlWorkflows — models/<model>/workflows/**
Workflows mirror the object layout exactly. A leaf workflow is type: wf; a group is type: group and declares its children.
models/<model>/workflows/<slug>.yaml # workflow (type: wf) or group (type: group)
models/<model>/workflows/<group>/<child>.yaml # nested workflow inside a group# models/deployment/workflows/sign-in.yaml (leaf)
type: wf
title: User sign-in
desc: How a browser request authenticates across the stack.
diagram:
syntax: sequenceDiagram
file: diagram.wf# models/deployment/workflows/billing.yaml (group)
type: group
title: Billing
workflows:
checkout: null # → models/deployment/workflows/billing/checkout.yamlAttachments
An attachment is any file that belongs to an entity and is not that
entity’s own file: not <page>.mdx and not <object>.yaml. A screenshot on
a spec page, a .draw diagram, an OpenAPI file, a protobuf schema, and an
object handbook are all attachments. The test is ownership, not the file
extension.
| Kind | Examples | Stored as | In a diff |
|---|---|---|---|
| Text attachment | .draw, .proto, openapi.yaml, handbook MDX | text, editable in Studio | line by line |
| Opaque attachment | .png, .webp, .webm, .pdf | bytes, verbatim | added / changed / deleted |
Declare attachments on an object or workflow under attachments: — the old
YAML key artifacts: is rejected (retired-yaml-key) and the object’s tabs
disappear until you rename it.
Naming by attachment kind
| Kind | YAML field | On-disk layout (inside the owner’s folder) |
|---|---|---|
| Workflow diagram | diagram.file | <key>/diagram.wf |
| MDX handbook / spec | attachments.<mdxKey> (type: mdx) | <key>/<mdxKey>/index.mdx (+ nested .mdx pages) |
| OpenAPI | attachments.<key> (type: openapi) | <key>/<artifactKey>.yaml |
| ERD | attachments.<key> (type: erd) | <key>/<artifactKey>.erd |
| Source text | attachments.<key> (type: sql / json / yaml / xml / protobuf / bpmn) | <key>/<artifactKey>.<ext> |
Declared attachments stay next to the owner’s YAML, under their own names
(mqtt_broker/api.openapi.yaml, mqtt_broker/guide/index.mdx). They do not
go into _attachments — that directory is hidden from navigation, and object
tabs must stay visible.
Embedded attachments (images, .draw inserted from a document) live in
_attachments/ inside that document’s directory. The link is always
./_attachments/<file>.
Workflow attachments
models/<model>/workflows/<…>/<wf_key>.yaml # workflow definition (type: wf)
models/<model>/workflows/<…>/<wf_key>/
├── diagram.wf # the sequence diagram
└── <mdxKey>/
├── index.mdx # an attached MDX specification
└── _attachments/ # images / .draw of that handbook pageFor example, the billing.checkout workflow:
models/deployment/workflows/billing/checkout.yaml
models/deployment/workflows/billing/checkout/
├── diagram.wf
└── runbook/
└── index.mdxObject attachments
models/<model>/objects/<…>/<obj_key>.yaml # object definition
models/<model>/objects/<…>/<obj_key>/
├── <openapiKey>.yaml # OpenAPI spec
├── <erdKey>.erd # ERD
├── <sqlKey>.sql # source-text attachment
└── <mdxKey>/
└── index.mdx # MDX handbookBecause every owner has its own folder, attachment keys never collide between sibling entities — the folder path already namespaces them.
`_assets` is gone
The attachments directory is _attachments. After migration the segment
_assets has no meaning: import does not treat it as attachments, export
does not write it, and it is not an alias. A leftover _assets folder is
just a hidden _ directory — it will not appear in the sidebar and will not
be owned as attachments. Bring an old tree to this layout with
pnpm normalize:specs.
Specs (documentation)
Specs are MDX pages organized into groups. Each group is a top-level folder
under specs/. Folders nest freely; ordering and titles come from meta.yaml.
specs/meta.yaml # group order + optional global labels
specs/<group>/meta.yaml # group title, icon, page order
specs/<group>/**/*.mdx # pages
specs/<page>/_attachments/* # every embedded attachment of that pageA page exists in exactly one of two forms, and the form follows the page’s content — it is not a choice:
| Form | When | Example |
|---|---|---|
| Flat file | the page has neither children nor attachments | specs/base/overview.mdx |
Directory with index.mdx | the page gained children or attachments | specs/base/diff-and-merge/index.mdx |
specs/base/
meta.yaml
overview.mdx ← most pages: nothing extra
diff-and-merge/
index.mdx ← this page has attachments
_attachments/changes-diff.png
_attachments/merge-flow.draw
conflicts.mdx ← and a child pageThe URL does not change when a page is promoted: /specs/base/overview is the
same for overview.mdx and overview/index.mdx.
Promotion is one-way. Deleting the last image or the last child does not collapse the directory back into a flat file. The user deleted content; they did not ask for a restructure.
Empty `meta.yaml`
A folder that exists only to hold attachments does not get an empty
meta.yaml. That file appears when the page has children to order
(pages: [limits]).
Adding a child to a flat page
Studio: context menu on the page → Add child → name. The tree grows a
nested node; you never pick a form or see index.mdx.
On disk, in one commit:
before after
specs/base/ specs/base/
meta.yaml meta.yaml ← unchanged: the page key is still overview
overview.mdx overview/
index.mdx ← the same page
limits.mdx ← new
meta.yaml ← pages: [limits]Doing it by hand:
mkdir specs/base/overview
git mv specs/base/overview.mdx specs/base/overview/index.mdx
printf 'pages:\n - limits\n' > specs/base/overview/meta.yamlIf the page is already a directory (it already had attachments or children),
there is no conversion: only limits.mdx is created and pages: is updated.
The child key index is reserved — that file is the parent page.
meta.yaml
Each pages: entry is one URL segment. A nested folder is listed by its
own key; that folder’s meta.yaml lists its children. Do not write fumadocs
nested references such as introduction/concepts.
# specs/base/getting-started/meta.yaml
title: Getting Started
pages:
- introduction
- quickstart
# specs/base/getting-started/introduction/meta.yaml
title: Introduction
pages:
- concepts| File / folder | Purpose |
|---|---|
specs/<group>/ | A documentation group (top-level sidebar section). |
meta.yaml | Folder title + pages: list controlling order and inclusion. |
index.mdx | The page itself when the page is a directory. |
<page>.mdx | A leaf page with no children and no attachments. |
_attachments/ | Images, video, PDF, .draw owned by that page. Names starting with _ are never pages. |
Pages vs. attachments
Any file or folder whose name starts with _ is a non-page resource and never
appears in the sidebar. Images live in _attachments/ inside the page’s
directory and are referenced as . The retired
folder name _assets and ?version=N on image URLs are not part of the
contract — git, Studio, and published sites all use the same relative path.
Naming rules (summary)
Keys are slugs
Object, workflow, and spec keys use lowercase letters, digits, and dashes (device-onboarding). The key is the file name (minus extension) and becomes part of the URL/path.
Folder mirrors key
An entity at <key>.yaml owns the sibling folder <key>/ for its children and attachments. Groups use the same rule.
Declarations are the source of truth
The parent’s objects: / workflows: map decides which sibling files load and in what order. Keep it in sync when adding or removing files. Specs are the other way around: files are discovered, and meta.yaml only sets order.
Reserved names
diagram.wf (workflow diagram), index.mdx (folder/handbook landing), meta.yaml (ordering), and _-prefixed folders (attachments) are reserved. A child spec page cannot be named index.
Last updated on