Writing in KM
Python asks you to program. YAML asks you to accept a tree of eight files and an indentation that counts. KM is a third door: a module fits in one file, read from top to bottom.
It is not an engine. A .km file is read, translated into exactly the documents the YAML
loader already consumes, and handed to that same loader. No new semantics, no new security
model, no eval.
.kw ──▶ ┐ .yaml ──▶ ├──▶ the same documents ──▶ the kernel's real functions .py ────┘The library module, whole
Here is the module from steps 1 to 6 of this walkthrough, in its KM form. This is the real file: it installs, it creates the same tables and it gives the same screens.
module library { title "Library" version 0.1.0 depends core}
model library_title "model.library_title" { taggable gallery cover
field name text required max 200 field author text required max 120 field isbn text unique max 17 field published_year number field genre choice fiction "Fiction" | science "Science" | history "History" | children "Children" | reference "Reference" default fiction field summary text field cover file extensions png, jpg, jpeg, webp, svg maxsize 5242880
search title from name subtitle from author}
model library_copy "model.library_copy" { field title_id link to library_title required field barcode text required unique max 32 field shelf text max 16 field condition choice new "New" | good "Good" | worn "Worn" | lost "Lost" default good field acquired_on date
compute name text max 64 stored = concat(barcode, ' - ', shelf)}
view library_title list { columns name, author, genre, published_year, isbn}
view library_title form { section section.work fields name, author, genre, published_year, summary section section.reference fields isbn, cover}
view library_loan timeline { start taken_on end due_on label member_id groups by state zoom week}
group library_reader "group.library_reader"group library_librarian "group.library_librarian"
access library_title { library_reader read library_librarian read, create, write, unlink kws_admin read, create, write, unlink}
workflow library_loan_lifecycle on library_loan { start out states out, returned, lost
transition check_in from out to returned transition declare_lost from out to lost}
translate fr { model.library_title "Catalogue" section.work "L'œuvre" workflow.check_in "Rendre"}Four models, twelve screens, four permission sets, a lifecycle and two languages, in one file. That is the only argument that matters against YAML.
What it gives, and how we know
The KM module was installed beside its Python twin, on the same database:
same tables : yes (library_title, library_copy, library_member, library_loan)column differences : 0permission differences : 0Then exercised for real, through the gated session:
title : Dune | genre fictioncopy : computed field -> 'C-001 - A3'loan : state outout -> check_in : returnedreturned -> check_in : no such transitionThe computed field is worth a look. The Python twin produces it with a function; here it is
the expression concat(barcode, ' - ', shelf), and the result is the same. That is the
whole argument of the format: the few percent of logic go through the expression language that
already exists, without KM having to know how to program.
The grammar in ten minutes
A module, then models. A block opens with a keyword and a brace. A statement fits on one line and ends there: no dashes, no meaningful indentation.
A field is a sentence. field code text required unique max 32 reads. Types carry the
names a person would say: text, number, money, date, yesno, choice, file.
Relations are phrases: link to, has many … via, many to many, count of.
The kernel’s own names still work. char, integer, selection are accepted too: the
format adds words, it hides none.
A long list continues on the next line when the break falls on | or ,.
Expressions are not re-invented. concat(a, b), count(lines), today(), user.name:
that is the kernel’s expression language, carried through untouched.
The errors are the real deliverable
A language is judged on its errors. This one names the line, shows the offending line, points at the column and offers the word that would have worked:
line 6: field 'name': unknown type 'txt' field name txt ^ did you mean 'text'?When no word is close enough it lists the ones that exist instead of guessing. A block of the
language that is not implemented yet is refused by name, with its lot: “menu is part of
the language but is not implemented yet — lot K3”. Neither “unknown keyword”, which would read
as a typo, nor silence, which would read as a feature that shipped.
What KM covers, and what it does not
| Area | KM |
|---|---|
| Models, fields, relations, computed fields | yes, all 20 declarative types |
| Screens (list, form, kanban, calendar, timeline, workload, pivot, dashboard) | yes |
| Groups, model rights, field restrictions, row rules | yes |
| Workflow, actions, automations, scheduled jobs | yes |
| Translations, installed and demo data | yes |
| Menus, PDF reports, extending another module | not yet (lots K3 to K5) |
| Reading a document, calling an AI, talking to a service, a real algorithm | no, deliberately |
That last line is not a gap to fill. A language that expresses arbitrary computation is a programming language, and it would be a worse one than Python. The planned answer is an escape hatch: the file names a handler, it does not contain one, and the kernel resolves it. It lands in lot K8.
The tools
Four commands, each taking a file or a directory:
kewos km check modules/library # reads the file and says what is wrong, with the linekewos km fmt modules/library # lays it out canonically, in placekewos km fmt --check modules/library # writes nothing, exits non-zero if a file would changekewos km convert modules/library_yaml --out modules/library.kmkewos km show modules/library # the documents it lowers to, as JSONkewos km complete modules/library/library.km --line 12 --column 8fmt settles the layout so that nobody has to: indentation by depth, one column for field
names, one blank line between blocks. Formatting twice changes nothing, and comments stay
where you put them. A file the parser cannot read is returned untouched; check is what
tells you why.
--check is the form for continuous integration. It touches nothing and names the files
that are not in shape.
The escape hatch
KM holds no code, and never will. For the few percent of logic that needs a real function, the file names a treatment instead of writing it:
handler invoice_loan in billing
action invoice on library_loan "action.invoice" { run invoice_loan}in billing means the file billing.py in this module’s own directory, and nothing else.
A dot, a slash or a .. is refused while reading: the reachable namespace is the module’s
directory. A module authored from Builder lives in the database and has no directory, so the
hatch does not exist for it.
If the file or the function is missing, the module refuses to load. That is deliberate: a button that fails the day somebody uses it costs more than a module that refuses to start.
And because every hatch is declared at the top of the file, you see every place where code runs in one glance. The price, stated plainly: a module that uses the hatch is no longer “just data”, it ships Python, and the package signature is what answers for it. See ADR 0300.
Choosing
Nothing forces a once-and-for-all choice: the three formats live in the same installation, see each other through events and the registry, and deploy the same way. See Python, YAML or KM.