Skip to content

The template context

When you generate code, Restorm passes your entire design to the template as a ctx object. Your models, your enums, your routes and above all their intentions (searchable, PII, cache, and the model’s whole operational profile: volume, access pattern, traffic, freshness, sensitivity, retention) are there — a template reads them to decide what to generate. No variable needed for that: the information you’ve already entered in the design is directly available.

A property's Code generation tab: its flags and its variables read by the template

ctx
├─ design # name, version, description, basePath, defaultAuth, vars
├─ enums[] # name, description, values[]
├─ models[] # name, description, inherits, identifier, timestamps, softDelete,
│ # volumetry, accessPattern, traffic, freshness, sensitivity,
│ # retention, properties[], allProperties[], examples[], vars
└─ groups[] # name, basePath, versionPrefix, headers[], routes[]
└─ routes[] # method, path, fullPath, params[], body, responses[],
# auth, pagination, cacheSeconds, idempotent, deprecated, vars

References are by name (never by internal identifier): property.enum is the enum’s name, property.references.model the name of the target model, model.inherits the parent’s name. On a model, properties[] lists its own properties (for the type declaration) and allProperties[] the own + inherited, flattened properties (for a complete instance: example data, SQL columns, request body).

Boolean flags are present in ctx only when they are enabled. The engine renders in StrictUndefined: so you must test for the presence of the key, never its value.

{# ✅ correct — testing for presence #}
{% if 'searchable' in p %}INDEX({{ p['name'] }}){% endif %}
{# ❌ wrong — raises an error when the flag is absent #}
{% if p.searchable %}…{% endif %}

Only property.required (boolean) and route.auth (enumeration) are always present. The other boolean flags are “present = true”; the value fields (volume, operational profile, cache…) carry their value when they’re filled in, and are absent otherwise — same presence rule.

On a propertyOn a modelOn a route
readOnly · writeOnly · nullabletimestamps · softDelete (booleans)idempotent · deprecated (booleans)
unique · searchable · immutable · piiOperational profile (values): volumetry · accessPattern · traffic · freshness · sensitivity · retentioncacheSeconds (number) · pagination (object)

A model’s operational profile values: accessPattern (readHeavy / writeHeavy / balanced / appendOnly), traffic (low / medium / high), freshness (strong / shortCache / longCache), sensitivity (public / internal / confidential / pii), retention (permanent / archivable / ephemeral), volumetry (hundreds / tenThousands / millions).

Two cases, depending on whether the information already exists in the design or not.

If you’ve set a cache duration on a route, it arrives in route['cacheSeconds']; route['idempotent'] tells you it’s safe to cache (read-only):

{% for group in ctx['groups'] %}
{% for route in group['routes'] %}
{% if 'cacheSeconds' in route and 'idempotent' in route %}
// {{ route['method'] }} {{ route['fullPath'] }}
app.use("{{ route['fullPath'] }}", cache({{ route['cacheSeconds'] }}));
{% endif %}
{% endfor %}
{% endfor %}

b) “Read-heavy” is a native field of the model

Section titled “b) “Read-heavy” is a native field of the model”

No variable needed: “read-heavy” is directly the model’s access pattern. In the model’s settings, Operational profile group, you choose Read-heavy — the template reads it in model['accessPattern']. Combine it with model['freshness'] (freshness tolerance) to decide whether to cache, and for how long:

{% for model in ctx['models'] %}
{% if model['accessPattern'] == 'readHeavy' and model['freshness'] != 'strong' %}
{% set ttl = 3600 if model['freshness'] == 'longCache' else 60 %}
registerCache("{{ model['name'] }}", {{ ttl }}); // cache enabled ({{ ttl }}s)
{% endif %}
{% endfor %}

Note the != 'strong' test: a read-heavy model but with strong consistency must not be cached. That’s the whole point of having both intentions side by side in the design.

Reserve generation variables for what the design doesn’t already carry (a template-specific setting): the business intentions — volume, access pattern, traffic, freshness, sensitivity, retention — are native fields of the model.

This context is versioned (ctx['contextVersion']) and described exhaustively by a JSON-Schema shipped in the application repository (docs/contributing/design-codegen-context.schema.json) — copy it into your template so its CI validates the reference context. An AI agent gets the same contract on the fly via the MCP tool get_design_template_contract.