Skip to content

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 ​

#ChapterYou writeYou see or run
1An empty file and an architectureOne system and two containers (C4)The architecture view, with the health badge green
2The dataAn ExpenseShape in SHACL, then a deliberate mistakeThe badge turning red and naming the element, then green again
3The screensAn IFML page with a form and a list, and the operations it callsA generated app whose form rejects invalid input and stores valid input
4The processA BPMN process triggered from the form, with an approval user taskThe submission running through SpiffWorkflow, approved from the /tasks inbox
5The rulesA DMN table that routes approvals by amount, and a gateway with one path per routeDifferent paths for different amounts, with no code change
6The lifecycleA statechart: draft, submitted, approved or rejected, paid; the tasks that fire each transition; a block guarded by stateEach path ending in its state, a block shown only while submitted, an illegal transition refused
7People and permissionsEmployee, manager and finance roles, a policy on the submit operation and on each user task, and a payment taskA 403 for the wrong role, an expense ending paid, and the viewer naming the screens an employee can use that write a payment
8Growing upA 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
9Hand-written codeA 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 itThe manager's notification stored before the approval task opens, seams generate leaving your code alone, and an unsendable notification taking the expense to finance
10Expenses in SemPKMA 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 itExpenses 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
11A second bounded contextTwo 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 processThe 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:

    sh
    uv sync --all-packages
    source .venv/bin/activate
  • A 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.trig

Each checkpoint is the previous one plus that chapter's additions, so a diff between two checkpoints shows exactly what a chapter adds:

sh
diff examples/expenses/chapter-01/expenses.trig examples/expenses/chapter-02/expenses.trig

From 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.

Released under the Apache-2.0 License.