Skip to content

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 : 0
permission differences : 0

Then exercised for real, through the gated session:

title : Dune | genre fiction
copy : computed field -> 'C-001 - A3'
loan : state out
out -> check_in : returned
returned -> check_in : no such transition

The 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

AreaKM
Models, fields, relations, computed fieldsyes, all 20 declarative types
Screens (list, form, kanban, calendar, timeline, workload, pivot, dashboard)yes
Groups, model rights, field restrictions, row rulesyes
Workflow, actions, automations, scheduled jobsyes
Translations, installed and demo datayes
Menus, PDF reports, extending another modulenot yet (lots K3 to K5)
Reading a document, calling an AI, talking to a service, a real algorithmno, 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:

Fenêtre de terminal
kewos km check modules/library # reads the file and says what is wrong, with the line
kewos km fmt modules/library # lays it out canonically, in place
kewos km fmt --check modules/library # writes nothing, exits non-zero if a file would change
kewos km convert modules/library_yaml --out modules/library.km
kewos km show modules/library # the documents it lowers to, as JSON
kewos km complete modules/library/library.km --line 12 --column 8

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