Codegen & the YAML contract
Chemistry’s defining rule: an element is declared in YAML, and a generator compiles that declaration into the code the rest of the platform runs. Nothing about an element type is hand-written in the runtime or the UI. This page is the reference for that contract — the pipeline, the rule that keeps it honest, and why the whole platform can be element-agnostic because of it.
The pipeline
chemistry/elements/**/.triform/*.yaml → chemistry/generators/rust/ → chemistry/generated/
(the declaration) (the codegen binary) (NEVER edit by hand)
Each element type carries a small set of .triform/*.yaml files — definition.yaml
(identity, shell role, runtime wiring), properties.yaml (what you can configure),
contract.yaml (ports and wires), ops.yaml (operations and their input schemas),
validation.yaml, observability.yaml, and the authored docs.yaml narrative
overlay. Running chemistry/generators/build-all.sh reads all of them and regenerates
everything under chemistry/generated/ — the Rust types, the JSON schemas, the API
routes, the category-dispatch maps, and the docs corpus.
Two rules keep it honest
- Never edit
chemistry/generated/. It is overwritten on every build. You change the YAML (or the generator), regenerate, and commit the source and the generated output together in one change, so the contract and its compiled form never drift apart. A generated file edited by hand is a change that vanishes on the next build. - The generator owns policy; the runtime owns mechanism. Physics and the Portal
never branch on a specific element type. All element behaviour flows through the
generated dispatch — a category lookup, a generated schema, a generated route — not
a hand-written
if element.kind == "python". To add behaviour, you add it to the YAML or the generator, never by special-casing an element downstream.
This is the lockstep: the declaration and the running code are the same fact in two forms, regenerated together, and that is what lets one URL shape, one run model, and one set of docs sections describe a Python action, a Postgres table, a Slack connector, and a circle identically.
Why this makes the platform element-agnostic
Because every element type is generated from the same YAML shape, the runtime can
treat them uniformly. The dispatch layer reads an element’s category from generated
metadata and routes accordingly — it never needs to know that python runs in a
sandbox and sql runs in Postgres at the level of a hand-written branch; that
knowledge lives in the element’s declaration and flows through generated code. The
same is true of this documentation: the per-element pages, the operation index, the
capabilities catalog, and the topology facts are all generated from the YAML, so
the docs and the runtime can never disagree about what an element does.
Add a new element type and you do not touch physics or portal — you write its YAML, run the generator, and the new type appears in the API, the dispatch maps, the docs, and the agent tools at once.
One load-bearing exception
There is exactly one hand-maintained fork of generated output:
portal/src/generated/api/ is a hand-edited copy of
chemistry/generated/portal-api/ and must not be auto-synced — a warning in
build-all.sh marks it. Everything else under chemistry/generated/ is
regenerate-only.
Related
- Pillar: Chemistry overview — the catalog this contract produces
- Pillar: Compound elements — parent types with nested children
- Concept: elements — the declarative unit the contract compiles
- Concept: composition — how the generated elements relate