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.
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:
| Field | Value |
|---|---|
| Name | Book |
| Key | lib_book |
Then two Attribute types:
| Name | Key |
|---|---|
Title | lib_title |
Detail | lib_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:
| Field | Value |
|---|---|
| Name | Shelf |
| Key | lib_shelf |
| Data type | string |
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:
- Name it
Dune. - Leave the description empty.
- 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:
| Attribute | Attribute type |
|---|---|
title | Title |
author | Detail |
year | Detail |
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.
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.
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:
- Click Generate All and wait for the green tick.
- 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 made | And it decided |
|---|---|
The entity type lib_book | Which entities the pack produces a file for |
The attribute types lib_title / lib_detail | What each attribute becomes in the card |
The custom property lib_shelf | A fact about the book that reaches the output but is not a field of it |
| The definition | The rule tying a type to a template |
| The builder and template | How 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.