Skip to main content

Your own model, from nothing

About 45 minutes. The first walkthrough installed a finished demo and changed it. This one builds everything from an empty model: you define the types, add the custom properties, create a viewpoint, model a few entities, and then write a small template pack of your own that turns them into files.

The goal is to see where each piece comes from. In the demo, the types and the pack were handed to you. Here you make them, and by the end you will have generated a file from a model and a template you wrote yourself.

The example is a tiny library: books, with a title and a couple of details, shelved somewhere. Small on purpose — one entity type, two attribute types, one custom property is enough to show every idea.

Two halves

Steps 1–6 happen entirely in the browser. Step 7 — writing the template — is code and a one-line upload from a terminal, because ITB has no in-app editor for pack code. The break is called out where it happens.


1 · Start from an empty model​

If you have a model open you want to keep, save it first with Model Actions ▸ Download Model in the header toolbar. Then, to start clean, either open a fresh workspace or clear the current one.

An empty model generates nothing, because nothing is installed and nothing is typed. That is the starting point: every file you produce by the end exists because you added the type, the entity and the rule that make it.


2 · Define the types​

Open Settings (gear icon in the left sidebar) and go to the Types tab. Four categories down the left: Entity, Attribute, Relation, Viewpoint. A type is a piece of vocabulary — a name your model can use and a pack can match on.

Add one Entity type:

FieldValue
NameBook
Keylib_book

Then two Attribute types:

NameKey
Titlelib_title
Detaillib_detail

One entity type: the name you see, and the key a pack matches on.

The attribute types have their own category in the same tab.

The key is the part that matters, and it is worth understanding now rather than later. The name is what you see; the key is what a pack matches on. Later, the template pack will say "produce a card for every lib_book" — it names the key, never the display name. So you can rename Book to Volume in this dialog and nothing breaks, because lib_book did not change.

We are not adding a relation type — the library model does not need one yet. One entity type and two attribute types is a complete vocabulary for step 7.


3 · Add a custom property​

Still in Settings, switch to the Custom Properties tab. Add one, scoped to entities:

FieldValue
NameShelf
Keylib_shelf
Data typestring

Custom properties are their own tab, with categories for entity, attribute, relation and model.

A custom property is a statement about an entity rather than a field of it. A book's title is part of the book; where it is shelved is a fact you keep about it. That distinction is the whole reason custom properties exist, and in step 7 you will see the shelf reach the generated card while never being a "field" of the book.


4 · Create a viewpoint​

Close Settings. In the left sidebar, under Viewpoints, click + and name it Library. Open it.

A name is enough to create one.

A viewpoint is a canvas — a view onto the model. Entities live in the model; a viewpoint decides which of them you are looking at and where they sit. You need one open before you can place anything, which is why the canvas said No viewpoint open until now.


5 · Model a book​

In the left sidebar, click + next to Entities and choose Add Entity. A dialog asks for three things:

  1. Name it Dune.
  2. Leave the description empty.
  3. Set Type to Book — the entity type you defined in step 2. Then Create Entity.

Now select Dune in the tree, so the right sidebar shows its panel, and open its Attributes tab. Add one attribute per row, giving each a type:

AttributeAttribute type
titleTitle
authorDetail
yearDetail

Name, and the type from step 2. The type is what decides whether a pack ever produces a file for it.

The first attribute, on the Attributes tab. Each one carries a type of its own — that type is the role it plays in the output. The other two go in the same way.

The attribute type is what the pack will read — title carries lib_title, the other two carry lib_detail. An attribute you leave untyped is not an error, but step 7's pack will skip it: it has no role, so there is nothing to render for it.

Add one more book the same way — Foundation, type Book, a title and an author — so that generation later produces more than one file.

note

The canvas still says the viewpoint is empty, and that is correct: an entity belongs to the model, and a viewpoint only shows the ones you put on it. Drag a book from the tree onto the canvas if you want to see it there. Generation reads the model, so it does not care either way.


6 · Set the custom property​

Select Dune and stay on the Overview tab of the right sidebar. Under Properties, click Add Property, pick Shelf, and give it the value SF-3. Leave Foundation's shelf empty.

Defining the property (step 3) and giving it a value are two different acts, in two different places.

That empty one is deliberate. In step 7 the template shows the shelf only when it is set, so Dune's card will carry a shelf line and Foundation's will not — the same template, two different outputs, decided by the data.

You now have a complete, typed model. Nothing has generated yet, because nothing knows how to turn a Book into a file. That is the pack, and it is the rest of the walkthrough.


7 · Write a template pack​

This is the step that leaves the browser. A template pack is three files in a folder:

library_cards/
config/config.json the vocabulary and the rule
__init__.py the builder (Python)
templates/book_card.md.jinja2 the template

Full anatomy is on Writing a Template Pack; here we build the smallest one that works.

The configuration​

config/config.json declares the same types you created in the UI — so the pack and the model speak the same vocabulary — and one definition, the rule:

{
"version": 1,
"metadata": { "name": "library_cards", "version": "1.0.0", "display_name": "Library Cards" },
"requirements": {
"entity_types": [ { "name": "Book", "key": "lib_book", "match_by": "key" } ],
"attribute_types": [ { "name": "Title", "key": "lib_title", "match_by": "key" },
{ "name": "Detail", "key": "lib_detail", "match_by": "key" } ],
"entity_custom_properties": [ { "name": "Shelf", "key": "lib_shelf", "data_type": "string" } ]
},
"definitions": [
{
"name": "book_card",
"type_key": "lib_book",
"template": "book_card.md.jinja2",
"context_builder": "build_book_card_context",
"name_pattern": "{name}.card.md"
}
]
}

The definition reads: for every entity of type lib_book, call build_book_card_context, render book_card.md.jinja2, and name the file after the entity. type_key is lib_book — the key from step 2, not the word "Book".

The builder​

__init__.py turns a book into the data the template needs. It must export create_pack, and one function per definition:

import os
from pydantic import BaseModel
from template_engine_api import TemplatePack, TransformHelpers

CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config", "config.json")
TEMPLATES_DIR = os.path.join(os.path.dirname(__file__), "templates")


class BookCardContext(BaseModel):
title: str
details: list[dict]
shelf: str


class LibraryCardBuilders(TransformHelpers):
def build_book_card_context(self, entity_id: str) -> BookCardContext:
entity = self._find(entity_id)
title = ""
details = []
for attr in self._attrs(entity_id):
role = self.imh.type_key(attr.id) if attr.type else None
if role == "lib_title":
title = attr.name
elif role == "lib_detail":
details.append({"name": attr.name, "value_type": attr.datatype or "string"})
return BookCardContext(
title=title or entity.name,
details=details,
shelf=self._cp(entity_id, "lib_shelf", ""),
)


def create_pack(data_model, config=None) -> TemplatePack:
builders = LibraryCardBuilders(data_model, config=config)
return TemplatePack(
context_builders={"build_book_card_context": builders.build_book_card_context},
template_config=CONFIG_PATH,
template_loader_path=TEMPLATES_DIR,
)

Read the loop: self.imh.type_key(attr.id) asks each attribute what role it plays and gets back the key — lib_title or lib_detail. The title becomes the heading; the details become rows. An attribute with no type returns None and is skipped. The shelf is read with _cp — a custom property, not an attribute — with an empty default.

caution

Do not add from __future__ import annotations. It turns the annotations into strings and Pydantic then cannot resolve BookCardContext when the pack is loaded, failing with "…Context is not fully defined".

The template​

templates/book_card.md.jinja2 turns that context into a file. The builder's return value arrives as ctx:

# {@ ctx.title @}

{= if ctx.details =}
| Detail | Type |
| --- | --- |
{= for d in ctx.details =}
| {@ d.name @} | {@ d.value_type @} |
{= endfor =}
{= else =}
_No details recorded yet._
{= endif =}

{= if ctx.shelf =}
Shelf: **{@ ctx.shelf @}**
{= endif =}

The delimiters are {@ … @} for an expression and {= … =} for a block — not the usual {{ }} and {% %}. If you write {{ ctx.title }} out of habit it will not error; it will render empty, and the mistake looks like the builder failed to supply a title.

Upload it​

Zip the folder and post it to your instance. It installs for your user only:

cd library_cards && zip -r ../library_cards.zip .
curl -X POST "$ITB_API/api/v1/template/packs" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@../library_cards.zip"

($ITB_API is your instance's base URL; $TOKEN is a bearer token for your session.)


8 · Generate, and check the result​

Back in the browser:

  1. Click Generate All and wait for the green tick.
  2. Download Artifacts and unpack.

Two files, one per book:

Dune.card.md
Foundation.card.md

Open Dune.card.md: the title as a heading, author and year as rows, and a Shelf: SF-3 line. Open Foundation.card.md: its rows, and no shelf line — because you left that custom property empty, and the template shows it only when set.

Before generating you could also have asked: right-click Dune's title bar on the canvas and choose Show templates. It lists your book_card definition, the artifact it would produce, and the pack it came from — the same Show Templates panel from the first walkthrough, now pointed at a rule you wrote.


What you just built​

You madeAnd it decided
The entity type lib_bookWhich entities the pack produces a file for
The attribute types lib_title / lib_detailWhat each attribute becomes in the card
The custom property lib_shelfA fact about the book that reaches the output but is not a field of it
The definitionThe rule tying a type to a template
The builder and templateHow a book turns into a card

That is a whole pack — the same shape as the demo packs, only smaller. From here, Pack Configuration covers the levers in depth, and Writing a Template Pack covers the anatomy, the scopes, and how to test a pack before you ship it.