Skip to main content

How Generation Works

Generation takes two inputs and produces files. The model is yours; the rules come from a pack. Nothing in the middle is specific to Data Vault, to JSON Schema or to any other output — that is why installing a different pack changes what you get without changing the application.

The path from model and pack to artifactsThe model and the pack configuration both feed the dispatcher. The dispatcher matches definitions against the model, calls the pack's context builder for each match, renders the template, and writes one artifact per match.Your modelentities, attributes,relations, propertiesThe packconfig.json — typesand definitionsbuilders + templatesDispatcherfor every definition:does it match?scope decides howoften it runsRendercontext builderreads the modeltemplate turns itinto textArtifactsone file per match,named by pattern

The two questions a definition answers​

A pack's definitions are the rules. Each one answers two questions, and everything about generation follows from them:

Which model elements does this apply to? A definition names a type_key, and an entity whose type carries that key matches. It can narrow further, and these are the conditions available — the same set the Explain why dialog reports on:

ConditionPasses when
type_keythe entity's type has this key
cp_equals / cp_not_equalsa custom property has, or does not have, this value
cp_existsthe custom property is set at all
effective_cp_equals / effective_cp_not_equalsthe same, after inheritance is applied
name_ends_withthe entity's name ends with this text
group_path_not_containsthe entity's group path avoids this text

They combine with ALL, ANY and NOT, so one entity type can be sent down different templates depending on how it is configured — the Data Vault pack does exactly that. What there is not is priority, fallback, or a default rule: a definition either matches or it does not.

How often does it run? That is the scope:

ScopeRunsProduces
entityonce per matching entityone file per element
modelonce, for the whole modelone file over everything

Every definition whose match applies generates. They do not compete and they do not know about each other, so two packs installed together simply both run — each quietly ignoring the types belonging to the other. "No template found for this type" is the correct answer, not a failure.

What runs per match​

For each match the engine calls the definition's context builder — a Python function in the pack — and hands the result to its template. The builder is where the model is read: it walks attributes, follows relations, reads custom properties, and returns a plain data structure. The template only formats what it is given.

That split is deliberate. A template that had to traverse the model would mix two hard things — deciding what is true, and deciding how it looks — and neither would be testable alone. With the split, a pack's tests exercise the builder against a stub model and never render anything.

info

The engine's Jinja delimiters are not the usual ones: an expression is {@ … @} and a block is {= … =}. That is so a template can emit dbt or Jinja code into the generated file without the engine consuming it. Standard delimiters do not raise — they render as literal text and the value comes out empty, so the failure looks like a context problem.

Where to go next​