A module's settings
Your module has choices that are not yours to make. How many days a loan lasts, whether a reminder goes out, which address the mail leaves from: those belong to the customer, not to your code. They have a screen, and that screen is declared in a file.
modules/library/├── module.json├── settings.json <- here└── models/{ "parameters": [ { "code": "library.loan_days", "type": "integer", "default": 21 }, { "code": "library.reminder_enabled", "type": "boolean", "default": true } ], "groups": [{ "id": "library", "label": "settings.tab.library", "order": 90 }], "forms": [ { "key": "library.settings", "label": "settings.library.title", "read_groups": ["kws_admin"], "write_groups": ["kws_admin"], "settings_group": "library", "settings_order": 10, "fields": [ { "code": "library.loan_days", "label": "settings.library.loan_days" }, { "code": "library.reminder_enabled", "label": "settings.library.reminder" } ] } ]}Restart: a Library tab appears under Settings, with its two fields and their defaults. You registered nothing, imported nothing, wired nothing.
The three sections
All optional, and each answers a different question.
parameters declares the parameter itself: its code, its type, its default. Five types, no
more: string, integer, decimal, boolean, json. The default is validated at boot, so a
typo surfaces at startup rather than at the first click.
groups creates a Settings tab. A module with a single form can skip it and sit inside an
existing tab.
forms is the screen: a title, who may read it, who may write it, and the fields. A field
does not redeclare its type: it points at a code, and the type declared above picks the editor.
Permissions
An empty read_groups means open to read. An empty write_groups never means open to
write: a form with no write list is editable by nobody. That is deliberate, and it is the right
default: a forgotten setting is read-only, not wide open.
A field can do a little more
{ "code": "mail.provider", "label": "settings.mail.provider", "choices": [["smtp", "SMTP (your server)"], ["console", "Console (dev)"]]},{ "code": "mail.smtp.host", "label": "settings.mail.smtp_host", "show_when": { "field": "mail.provider", "in": ["smtp"] }},{ "code": "mail.smtp.password_secret_ref", "label": "settings.mail.password", "secret": true}choicesturns the field into a dropdown.show_whenshows the field only while another one holds one of the listed values. A hidden field is never required, so it can never block a save.secret: trueis the one case where the value does not travel back. What the administrator types goes encrypted into the vault (kewos/secrets, rule 16), the parameter keeps only a reference, and reading the form returns a plain “configured” boolean. An SMTP password never comes back in an HTTP response.
Reading the setting from your code
from kewos.config import get_parameter
days = get_parameter(engine, "library.loan_days") # tenant defaultdays = get_parameter(engine, "library.loan_days", company_id) # that company's overrideResolution is a three-step cascade, run here on a real instance:
before any write : 21 <- the value declared in the fileafter a tenant write : 30company 1 (inherited) : 30 <- no override: it inheritscompany 1 (overridden) : 7tenant default : 30 <- untouched by the overrideSo one company can have its own loan length without moving the others, and you wrote no cascade logic at all.
What the file cannot do
A validator. If your parameter must refuse certain values (“at least one day”), JSON cannot say so. You declare it in Python instead, and the file merely references it in a form:
def register(registry): register_parameter( "library.loan_days", ParameterType.INTEGER, default=14, validator=_refuse_zero, )The kernel reads your settings.json after your register(), precisely for this: your
Python declaration wins, and the file’s duplicate is skipped silently. The validator survives.