A Dialect adds architectural meaning to plan or state instances. Start with a small provider case whose exported plan you can inspect. Build Rules from that evidence, not names or a desired diagram shape.
For each Rule, verify:
- exact instance eligibility;
- architectural classification, emission, or composition it contributes;
- source path proving each result.
Start with Read a Rule and Evidence and target resolution if the connection between a provider attribute and an architectural fact is unclear.
- Dialect package Filesproject/└── dialects/└── payments/├── dialect.rf.hcl├── vocabulary.rf.hcl├── network/│ └── virtual-network.rf.hcl└── presentation.json
Run the commands below from
project/. Files can divide the source for review, but onedialectdeclaration owns the entire recursive source tree. - dialects/payments/dialect.rf.hcl RFdialect "payments" {version = "0.1.0"provider "hashicorp/aws" {version = "= 6.62.0"}}
A Dialect cannot import another. References can target its own definitions or the embedded RF Vocabulary; Rootform records the latter dependency from
rf.*references. The provider constraint should match versions you tested. -
Use the RF Vocabulary when its terms match what your provider evidence can establish:
rf.concept.virtual-networkandrf.concept.subnet;rf.concept.kubernetes-cluster,rf.concept.managed-database,rf.concept.object-storage-container, andrf.concept.service-identity;rf.context.networkandrf.context.runtime.
Use a local Concept or Context for a term outside that shared vocabulary:
dialects/payments/vocabulary.rf.hcl RFconcept "load-balancer" {description = "A load-balancing service."}context "project" {description = "Placement in a provider project."}With the
paymentsowner above, these IDs arepayments.concept.load-balancerandpayments.context.project. Descriptions document meaning; they do not create architectural facts. - dialects/payments/network/virtual-network.rf.hcl RFrule "vpc" {match {kind = "resource"type = "aws_vpc"}as = rf.concept.virtual-networkidentity {attributes = ["id"]}endpoint {attributes = ["id"]}}rule "subnet" {match {kind = "resource"type = "aws_subnet"}as = rf.concept.subnetcontext {as = rf.context.networkto = rf.concept.virtual-networkvia = source.vpc_idon_null = "absent"on_empty = "absent"match {by = target.idstrategy = "exact"}}}
Match-only Rules are invalid.
match.kinddefaults toresource.match.typemust name the exact resource type exposed by the input adapter.wherecan narrow eligibility with a bounded, typed expression, but an unknown value does not establish a match.Every observed managed and data instance already has a Representation. A Rule enriches that same instance; lack of a matching Rule never erases it.
-
Use a Context for named placement, a Relation for a directed domain predicate, and a Contribution for support that does not absorb its source. First identify the target Concept or Rule in
to, then choose aviapath backed by the provider's evaluated value. Seton_nullandon_emptyon every emission: they say whether those values prove absence or leave the closure indeterminate.When a value identifies a target by attribute rather than a direct traversal, declare that target's identity attributes on its Rule. List alternative
match.bypaths in priority order. The supported comparison modes and their limits belong to explicit attribute matching. An unknown, sensitive, incompatible, or ambiguous candidate leaves the closure indeterminate. Do not turn it into a guessed edge.Use a
provider.<path>only when the provider configuration names a managed resource through a direct reference in a paired saved plan. Rootform can follow simple pass-through variables, locals, and module outputs for theplannedstage. A literal, transformed expression, state input, historical stage, OpenTofu providerfor_each, or JSON configuration syntax provides no such proof; the closure staysindeterminate (unavailable). Rootform does not read literal provider configuration values, whose sensitivity the plan export cannot identify. See traversal evidence.Allow an external endpoint only when the referenced object may truly be outside the plan's inventory. Choose its identity disclosure level deliberately; it never permits a sensitive value into a Form.
Choose a fact whose meaning matches what the provider evidence proves:
Direct parent proved by a resource reference RFcontext {as = context.ownershipto = concept.parentvia = source.parent_idon_null = "absent"on_empty = "absent"}- Use an ownership Context for an API or lifecycle parent when the traversal resolves that exact parent. A resource-group name is administrative placement only when the provider contract says the resource is created in that group.
- Use a domain Context such as
rf.context.networkwhen the reference proves placement in that domain. It may coexist with administrative ownership. - Keep an association with several beneficiaries as Contributions when no unique parent exists. If the API independently proves one parent, emit that Context and retain the distinct Contributions.
- Add a Relation only for a documented interaction. A shared traversal may prove both placement and interaction when those facts have different meanings.
For the direct-reference pattern above, a literal has no reference proof. An explicit identity match can establish a known literal value instead. Test missing, unknown, sensitive, transformed and ambiguous evidence beside the successful parent; never add a fallback parent from type or naming.
Composition members are ordered. An unresolved member stays on its root with a reason; dependent later members remain unresolved, while independent ones may resolve. The root keeps its classification and emissions. Members keep their Representations and do not inherit the root Rule or Concept. See Understand composition.
-
Check the source before interpreting a plan. The validation result identifies the first invalid path;
showthen confirms which qualified Rule and Concept the active catalog contains.The
aws_vpcandaws_subnettypes also match Rules in embeddedaws. A--dialectoverride adds a source for one command; it does not exclude that embedded owner. For this walkthrough, selectpaymentsand exclude embeddedawsin the project lock once, before analyzing or testing:Shellrootform fmt --check ./dialects/paymentsrootform validate dialects ./dialects/paymentsrootform validate rule payments.rule.subnet --dialect ./dialects/paymentsrootform show payments.rule.subnet --dialect ./dialects/paymentsrootform show rf.concept.subnetrootform add dialects ./dialects/paymentsrootform remove dialects aws --embeddedrootform init . --locked --offline --no-inputNamed commands use owner-first IDs. Bare names work only when unambiguous. The following plan and fixture commands use this recorded selection, without a
--dialectoverride.testreads selection from the directory under test. Usetest .fromproject/to keep the authoring selection while discovering nested fixtures. - Dialect fixture Filesfixtures/payments/minimal/├── main.tf├── plan.json├── plan.tfplan└── analysis.golden
Export
plan.jsonfrom a savedplan.tfplanand keep both beside the exactmain.tfused to produce them. OpenTofu users run the same commands withtofu. Saved plans and plan JSON can contain secrets in clear text; keep them out of Git and public artifacts. Rootform reads them locally; it never runs Terraform or OpenTofu and never contacts providers. The golden is a Form, not a copy of the plan. Plan inputs covers the export.Inspect one
rootform run fixtures/payments/minimal/plan.json --plan-file fixtures/payments/minimal/plan.tfplan --no-serveresult before recording the golden. Look for the expected instance, Rule, facts, and closure outcomes; a successful exit alone does not prove the Rule created the intended fact. Then record or compare fixtures:Shellrootform test . --updaterootform test .rootform test . --run payments/minimal--updatewrites the expected Form for review. The next command compares the Form of a real run against it; status0means every selected case passed,1means a case differed, and3meansrootform.lockis invalid or no case matched.--runnarrows cases by name. Review the golden diff before accepting a changed Rule. A--dialectoverride lasts one command and leavesrootform.lockunchanged;run,test,validate,list,show, andexplainaccept it. This walkthrough uses the lock because it must exclude the overlapping embedded owner. -
Optional
presentation.jsonmaps source resource identities independently of Rule coverage. It can also assign icons and labels to Rules and Concepts:dialects/payments/presentation.json JSON{"format_version": "1","resources": {"resource/aws_vpc": "generic/network","resource/aws_subnet": "generic/subnet"},"rules": {"vpc": "generic/network","subnet": "generic/subnet"},"concepts": {},"resource_labels": {"resource/aws_vpc": "Amazon VPC","resource/aws_subnet": "Amazon VPC subnet"},"rule_labels": {"vpc": "Amazon VPC","subnet": "Amazon VPC subnet"},"concept_labels": {}}Resource mappings work even when a resource type has no Rule. The manifest accepts no SVG, HTML, URLs, styling, layout, architectural facts, or behavior. Normal runs warn and ignore invalid presentation;
rootform package dialectsrejects it. See presentation contract. -
Packaging stays local and offline. This example binds the
paymentsowner to the AWS provider; the owner and provider are separate identities. Package the same source root you validated and tested:Shellrootform package dialects ./dialects/payments --to artifacts/oci \--source-url https://example.com/team/dialects \--documentation-url https://example.com/team/dialects/docs \--licenses MPL-2.0The result names the packaged Dialect and local destination. Before publishing, record the reviewed source revision with
--revisionwhen your publication process requires that provenance. Packaging itself sends nothing to a registry. Generic publication is separate:Shellrootform publish dialects artifacts/oci \--to registry.example.com/team/dialectsRootform has no official Dialect index or mutable discovery tag.
-
Continue from the same
project/directory with its plan fixture and lock. Replace the localpaymentsselection with the published reference. The lock keeps the exclusion of embeddedaws, so its Rules do not overlap with the published Dialect:Shellrootform remove dialects paymentsrootform add dialects \registry.example.com/team/dialects:dialect-payments-0.1.0rootform init . --locked --no-inputrootform run fixtures/payments/minimal/plan.json \--plan-file fixtures/payments/minimal/plan.tfplan \--require-enrichment --project . --locked --no-serve -o published-form.jsonInspect
published-form.jsonfor the samepaymentsRules and facts you reviewed before packaging. Rootform uses the recorded published identity; the local source directory no longer controls this run.The reference is illustrative; use the published reference you reviewed. Run
addfrom the same project root thatinitandrunuse. SetDOCKER_CONFIGbefore acquisition if the registry needs credentials.initacquires only recorded exact pins. See External content storage for locations.
See Test and validate, Dialect reference, and Rules.