Build an expense approval app
This tutorial builds a whole application from an empty file, one layer per chapter. Employees submit expenses, managers approve them and finance pays them out. Each chapter adds one model to a single TriG file, expenses.trig (until chapter 11 splits it in two), and ends with something you can see in the viewer or run in a browser.
If you want to see the pipeline end to end first, the quick start runs the finished chat app in about ten minutes. This tutorial is the slower track: you write the model yourself, and by the end you know why each triple is there.
The chapters
| # | Chapter | You write | You see or run |
|---|---|---|---|
| 1 | An empty file and an architecture | One system and two containers (C4) | The architecture view, with the health badge green |
| 2 | The data | An ExpenseShape in SHACL, then a deliberate mistake | The badge turning red and naming the element, then green again |
| 3 | The screens | An IFML page with a form and a list, and the operations it calls | A generated app whose form rejects invalid input and stores valid input |
| 4 | The process | A BPMN process triggered from the form, with an approval user task | The submission running through SpiffWorkflow, approved from the /tasks inbox |
| 5 | The rules | A DMN table that routes approvals by amount, and a gateway with one path per route | Different paths for different amounts, with no code change |
| 6 | The lifecycle | A statechart: draft, submitted, approved or rejected, paid; the tasks that fire each transition; a block guarded by state | Each path ending in its state, a block shown only while submitted, an illegal transition refused |
| 7 | People and permissions | Employee, manager and finance roles, a policy on the submit operation and on each user task, and a payment task | A 403 for the wrong role, an expense ending paid, and the viewer naming the screens an employee can use that write a payment |
| 8 | Growing up | A payouts channel, a payout process started from it with its own decision and payment lifecycle, a finance console, deploy facts, and where the data lives (a PostgreSQL store the shapes are seam:storedIn) | The payout running on its own after finance pays, the whole system under docker compose up, and the complexity check passing |
| 9 | Hand-written code | A Notify Manager service task whose operation is seam:implementedBy a Python function, the stub that function starts from, the function itself, and an error it declares with a boundary event that catches it | The manager's notification stored before the approval task opens, seams generate leaving your code alone, and an unsendable notification taking the expense to finance |
| 10 | Expenses in SemPKM | A deploy:SemPKM store and its triplestore, a URL binding that deploy:connectsTo it and a secret token binding that deploy:authenticatesTo it, and :ExpenseShape seam:storedIn it; a comm:SemPKMWebhook channel accepting :VendorShape and a vendor review process that seam:subscribesTo it | Expenses stored in SemPKM under the IRIs it mints and read back through SPARQL, the process unchanged, SemPKM's services in the compose files, /healthz reporting the store, and a vendor changed in SemPKM starting the vendor review |
| 11 | A second bounded context | Two domains, an expense and a finance context, what each seam:owns, a finance.trig with the finance ontology the expense ontology imports by version IRI, and Pay Expense moved to the payout process | The approval storm writing no payment, :payouts as the one line between the contexts, and seams check resolving the import from the dataset |
All eleven chapters are written. Chapter 8 ends with the whole system running under docker compose up and a model large enough to pass python -m seams_gen.complexity. Chapter 9 adds the one piece the model cannot generate: a function you write yourself. Chapter 10 moves the expenses into SemPKM without changing a line of code. Chapter 11 splits the model into two bounded contexts, one file each.
Prerequisites
A clone of the repository with the Python dependencies installed, as in Getting started:
shuv sync --all-packages source .venv/bin/activateA text editor. Every command runs from the repository root.
Some familiarity with Turtle helps but is not required. Each chapter explains the triples it adds. Authoring a model is the reference for the conventions.
Nothing in this tutorial needs Docker, an API key or a network service until chapter 8.
How checkpoints work
You grow one expenses.trig from chapter to chapter. The repository keeps a copy of the file as it stands at the end of each chapter, under examples/expenses/:
examples/expenses/chapter-01/expenses.trig
examples/expenses/chapter-02/expenses.trig
examples/expenses/chapter-03/expenses.trig
examples/expenses/chapter-04/expenses.trig
examples/expenses/chapter-05/expenses.trig
examples/expenses/chapter-06/expenses.trig
examples/expenses/chapter-07/expenses.trig
examples/expenses/chapter-08/expenses.trig
examples/expenses/chapter-09/expenses.trig
examples/expenses/chapter-10/expenses.trig
examples/expenses/chapter-11/expenses.trig
examples/expenses/chapter-11/finance.trigEach checkpoint is the previous one plus that chapter's additions, so a diff between two checkpoints shows exactly what a chapter adds:
diff examples/expenses/chapter-01/expenses.trig examples/expenses/chapter-02/expenses.trigFrom chapter 11 the checkpoint is a directory of two files, one per bounded context, which its seams.toml loads as one model (model = ".").
If your file stops working, compare it with the checkpoint for the chapter you are on, or copy the checkpoint and carry on from there.
The checkpoints cannot drift from the tutorial. CI parses each one, validates it against the seam shapes and opens it in the viewer (tests/test_expense_checkpoints.py). It also runs chapter 3's generated app (tests/test_expense_chapter3_app.py) and the process chapters' generated apps from chapter 4 on (tests/test_expense_process_app.py), checks chapter 6's lifecycle guard (tests/test_expense_lifecycle.py), chapter 7's roles (tests/test_expense_roles.py), chapter 8's payout (tests/test_expense_payout.py) and chapter 9's hand-written handler (tests/test_expense_handler.py, and tests/test_expense_e2e.py under docker compose) chapter 10's SemPKM store (tests/test_expense_sempkm.py) and chapter 11's contexts (tests/test_expense_chapter11.py), and checks that every model excerpt on these pages appears verbatim in its chapter's checkpoint (tests/test_expense_tutorial_doc.py).
Start with chapter 1.