2026-09-21 16:31:37 +02:00
---
name : portal-trainer
2026-09-24 00:45:16 +02:00
description : Onboard a business onto portal - interview them, write a plan, build the content repo (pages, aggregates.yaml, desks, mails) with the words written by a model, and score it until iris proves every need is met and a blind visitor routes the personas right. Use when someone wants a new portal site, a new flow on an existing one, a rebuild of a live one, or asks why visitors pick the wrong alternative.
2026-09-21 16:31:37 +02:00
---
# Portal trainer
You are onboarding a business onto [portal ](https://project.uhhm.no/uhhm/portal ):
a site that is one question per page, a few alternatives per question,
each alternative optionally a form whose answer is recorded into a
2026-09-24 00:45:16 +02:00
bucket with a state machine, worked from a group-gated desk, with a
mail to the person when their case moves.
2026-09-21 16:31:37 +02:00
You have a verifier. Use it instead of your judgment wherever it can
2026-09-24 00:45:16 +02:00
speak: `iris check --path <repo>/questions` (uhhm/iris, the release
matching the portal the site runs) exits non-zero until the structure
meets every need in `<repo>/needs.yaml` .
2026-09-21 16:31:37 +02:00
## The order of work
2026-09-24 00:45:16 +02:00
1. **Interview, then write the plan.** Never pages first. `plan`
reads a filled-in intake into `plan.yaml` (a model decides needs,
stages, forms and budgets; a person reads the result before
anything is built). By hand, the same questions: who comes to the
business and what each of them wants, who inside handles it, what
"handled" means (the states a case ends in), what it costs the
visitor to ask (`max_fields` , from the person, never from counting
the form you would like). Start from a `sectors/` file when there
is one.
2. **Say what the plan can say.** `does:` (one or two features, one
role each, the front door's opener that asks for nothing);
`shows:` (things to look at before asking); `doors:` only when the
offers are more than a page holds - measure it, a flat page with
ten offers scored worse than four doors; `mails:` per need (who
hears when a case enters a stage) with `mail: { from }` on the
plan; `moves_by: submitter` so the person has something to play
after sending; `invites_by` for onboarding; `events:` when
rebuilding a live site, so its records keep replaying.
3. **Write personas in the visitor's words, not the business's.** Two
or three per need, one who would not use the trade's vocabulary.
Never reuse a phrase you intend to put on a page. They are held
out from every model that writes words.
4. **Build.** `build --plan ... --lint iris` derives every file with
no model deciding structure; `--llm` lets a model write the words,
`--llm-for <path prefix>` only some of them (the mails alone);
`--words-from <live repo>` keeps a live site's hand-edited words
through a rebuild, following each card across pages; `--words
<json>` overrides slots. The builder refuses a reply in the wrong
language and puts back a submitter's link a model dropped.
5. **Score.** `iris check --needs-tasks` then `answer` , then
`--needs-score` , with and without descriptions; `rate --repo` for
readability and the four understanding questions, mails included.
A card with no button is context, never a choice. Trust the
descriptions score; the headings-only score swings by a third
between seeds. Before believing a change, `runs/spread.sh` it over
three seeds and require it to beat the spread. A change that does
not is rejected and kept as a run.
- A **misroute** is a wording defect until the same one survives
every rewording; then it is a seam in the plan.
- Change the page or the plan, never the persona.
6. **Keep the run.** `runs/<business>-<date>/` with the words as used
(`words.json` ), tasks, answers, scores per seed, and a README with
what moved and why. Runs are the record of what worked.
2026-09-21 16:31:37 +02:00
## What a page is
2026-09-24 00:45:16 +02:00
Full reference: portal's `README.md` ; a live example is
`uhhm/questions` . The tree under `questions/` is the router
2026-09-21 16:31:37 +02:00
(`index.yaml` is `/` , `_section.yaml` applies `qualifies` to a
directory, nested non-index files are followups).
```yaml
# questions/index.yaml
name : What brings you here? # the question - always a question
responsible : { name : ..., contact : ... }
alternatives :
2026-09-24 00:45:16 +02:00
- name : What we do # the opener: features, no button
features :
- { name : <a role's win>, description : <what we do for them> }
2026-09-21 16:31:37 +02:00
- name : <heading the visitor picks> # graded alone under --skim
description : <one or two sentences>
action : /received # where the submission lands them
consequence : [ Send] # the button
record_as : leads # bucket - must exist in aggregates.yaml
features :
- name : <form section>
requirements :
- { name : email, label : Email, type : email }
- { name : problem, label : What's wrong, type : textarea }
```
```yaml
# aggregates.yaml
aggregates :
- bucket : leads
initial : open
states :
2026-09-24 00:45:16 +02:00
open : { event : received, mail : { to : submitter, subject : "We have it" , body : "..." } }
2026-09-21 16:31:37 +02:00
quoted : { event : quoted }
2026-09-24 00:45:16 +02:00
won : { event : won, mail : { to : submitter, subject : "..." , body : "..." } }
lost : { event : lost }
2026-09-21 16:31:37 +02:00
transitions :
open : [ quoted, lost]
quoted : [ won, lost]
```
2026-09-24 00:45:16 +02:00
```yaml
# site.yaml
mail : { from : "Nordvik <post@nordvik.example>" , reply_to : ola@nordvik.example }
```
2026-09-21 16:31:37 +02:00
```yaml
# questions/desk/_section.yaml
qualifies : project_managers
# questions/desk/index.yaml
name : What came in?
alternatives :
- name : Leads
action : /desk
consequence : [ Confirm]
features :
- name : ""
resource :
source : { kind : kv, bucket : leads }
requires_group : project_managers
transitions :
- { to : quoted, label : Quote sent } # from defaults to open
- { from : quoted, to : won, label : Won }
```
## House rules for the copy
- The page name is a question the visitor could ask themselves.
2026-09-24 00:45:16 +02:00
- The front door opens modestly: what the business does for one or
two roles, then what there is to look at, then the offers. No
second tier.
2026-09-21 16:31:37 +02:00
- Headings say what the visitor gets, in the visitor's words. Lead
with what a thing enables; never define it by what it isn't.
2026-09-24 00:45:16 +02:00
- Every required field must be one the business will use to answer.
2026-09-21 16:31:37 +02:00
- A followup page says who reads it, when they answer, and what the
2026-09-24 00:45:16 +02:00
visitor can do meanwhile. A mail says what happened and, where the
person has a move, the link to make it.
- Code in English; the site in the business's language.
2026-09-21 16:31:37 +02:00
## Engines
2026-09-24 00:45:16 +02:00
Any engine that reads a task's `state` (`body` is the persona,
`page` what the page says before its options) and `questions` and
returns `{"id","choice","confidence"}` per line works. The visitor
and the writer must be different models. See the repo README for what
has been measured about each, and for the noise floor.