How Triform generates itself
The single most important thing to understand about Chemistry is that the platform is mostly generated from its own declarations. An element type is a handful of YAML files; a code generator reads them and emits the Rust types, schemas, routes, dispatch maps, and docs that the runtime and the UI actually run. This page is the teachable narrative of that loop — how a declaration becomes running code, and why that design makes the whole platform element-agnostic.
The loop
chemistry/elements/**/.triform/*.yaml → chemistry/generators/rust/ → chemistry/generated/
(source of truth) (the codegen binary) (NEVER edit)
You change a YAML file, run chemistry/generators/build-all.sh, and everything under
chemistry/generated/ is rebuilt from the declarations — the types, the JSON schemas,
the API routes, the category-dispatch maps, the topology rules, and the docs corpus.
The source and the generated output are committed together in one change, so the
contract and its compiled form never drift apart. The mechanics of which YAML kinds
exist and the never-edit-generated rule are the Codegen & YAML
contract page; this page is the why and the cascade.
The cascade: root → category → element
Not every fact about an element is declared on the element itself. Several behaviours cascade through three levels, narrowest-wins:
root (platform defaults) → category (e.g. all io elements) → element (this type)
Pricing, runtime/lifecycle, observability, and call-to-action all resolve this way. A
platform-wide default can be overridden for a whole category, and a category default
can be overridden for one element type. The generator resolves the cascade once and
records provenance — whether a given value came from the root, the category, or
the element — so a docs page or a runtime can say “inherited from io” or “overridden
here” rather than guessing. The resolver is shared (root → category → element with a
provenance tag), never re-implemented per element, so every surface that reads a
cascaded value reads the same answer.
This is why an element’s Pricing or Observability section can be honest about where its numbers come from: the cascade is computed at generation time, and the provenance travels with the value.
Generator owns policy; the runtime owns mechanism
The load-bearing design choice: all element-specific policy lives in the YAML and
the generator; physics and portal are mechanism, not policy. The runtime never
contains a line like if element.kind == "python". Instead it reads an element’s
category from a generated dispatch map and routes accordingly. Behaviour is added by
changing a declaration or the generator — never by special-casing an element type
downstream.
// WRONG — never in physics or portal
if element.kind == "project" { /* special handling */ }
// RIGHT — generated dispatch
match get_category_for_kind(&element.element_type) {
Some("actions") => { /* category-level logic */ }
_ => {}
}
That separation is what makes “add a new element type without touching physics or portal” true. Write the YAML, run the generator, and the new type appears in the API, the dispatch maps, the topology rules, the docs, and the agent tools at once — because all of those were generated from the same declaration.
Why it matters
Three properties fall out of this loop, and they are the reason the platform can stay coherent as it grows:
- One source of truth. The runtime, the docs, and the agent tools all consume the same generated output, so they cannot disagree about what an element does. The docs page for an element is not a hand-written description that can rot — it is generated from the same YAML the runtime obeys.
- Element-agnostic by construction. Because every type is generated from one YAML shape, the runtime treats them uniformly; the differences live in data, not in branches.
- Cheap, safe extension. A new element type is a new directory of YAML, not a cross-cutting change to the runtime — and the cascade means it inherits sensible defaults from its category without restating them.
Related
- Pillar: Codegen & YAML contract — the pipeline mechanics + the never-edit-generated rule
- Pillar: Chemistry overview — the catalog this loop produces
- Pillar: Compound elements — parent types with nested children
- Pillar: Topology — the residence + attachment rules generated from the same YAML
- Concept: elements — the declarative unit the loop compiles