Files
portal/runs/contractor-planned-2026-09-24/r3/llm/01-prompt.md
T
Bendik Aagaard LynghaugandClaude Fable 5.1 4d22f4cb44 plan: a spectator role goes back to the model once; bare-word movers and unlabelled groups parse
Re-planning the contractor showed three wasted rounds: `moves_by: {
checking: submitter }` written as a bare word, a group without a
label, and the two featured owner roles left with nothing to play,
which the builder only warned about after the fact. The bare word and
the missing label now parse (a label defaults to the id), and a plan
that parses with spectator roles is sent back once with the finding;
on the contractor, round three fixed both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 06:42:05 +02:00

9.4 KiB

plan.yaml

A plan is a needs spec that also says what each form collects. A builder turns it into a working site with no further decisions, so every structural fact about the business has to be in it.

business: <name>
language: en                  # the intake's language code
contact: { name: <who answers for the site>, email: <their email> }
about: >
  What the business does, in its own words.
mail: { from: "<name> <post@their-domain>", reply_to: <their inbox> }
                              # who every mail the site sends comes from; required as soon
                              # as any need has `mails`

does:                         # the front door's first card: what the business does, one
  - id: kebab-case            # or two features, one role each, no button. Say it for the
    for: <need id>            # role the business exists for and the next one it serves.
    text: what it does for them, in the intake's own words

shows:                        # things a visitor may look at before asking for anything;
  - id: kebab-case            # each `resource` is portal's resource block, verbatim
    text: one line on what it is
    resource: { public: true, source: { kind: url, url: https://... } }

doors:                        # a first question before the forms, only when the offers
  - id: kebab-case            # are more than one page holds (measured: four doors beat
    who: who comes through it # ten offers on one page; six offers on one page beat doors)

groups:                       # groups with a label the public may see: the committees
  - { id: traffic, label: Traffic, invited_by: [board] }   # a person can ask to join (optional)
invites_by: [board]           # groups whose desk may invite people into every desk

needs:
  - id: kebab-case            # ASCII only
    who: the kind of person, in plain words, in the intake's language
    wants: what they want from the business
    door: <door id>           # the door this public need sits behind; omit to put it on the front door
    enters_as: <group>        # ONLY for a role inside the business; omit for the public
    lands_in: <bucket>        # snake_case, ASCII
    handled_by: <group>       # the role that must see and move the record
    stages: [open, <stage>, ..., <end>, <end>]
                              # EVERY stage the business said a case goes through, first to
                              # last, read off their own sentences one verb at a time: "they
                              # come in, we go and look, we send a quote, we win or lose it"
                              # is [open, surveyed, quoted, won, lost]. A pipeline is not an
                              # inbox: "answered" is rarely the real outcome.
    done_when: [<end>, <end>] # every stage that counts as finished, good or bad
    success: [<end>]          # the finished stages that are a good outcome for the business
    skippable: [<stage>]      # stages a case may pass over (papers already complete)
    turned_down_from: [open, <stage>]
                              # the unfinished stages a case can still be turned down from;
                              # omit for every one. An applicant is declined before they
                              # start, not after - after, they depart
    moves_by: { <stage>: [<group>, ...] }
                              # only where someone other than handled_by moves a case INTO a
                              # stage: another group, or `submitter` (the person who sent the
                              # form; the form must then collect a required `email`)
    watched_by: [<group>]     # groups that see these cases and move nothing; `public` opens a
                              # read-only page of the bucket to everyone (contact details hidden)
    mails: { open: submitter, <end>: submitter, <stage>: <address> }
                              # who hears when a case enters a stage: `submitter` (the form
                              # must then collect a required `email`) or one address. Tell the
                              # person on arrival and on every decision that is about them.
    stages_text: { open: <words>, <stage>: <words> }
                              # each stage in the intake's language, where the ASCII id
                              # would read wrong to a person: "ikke_lost" is "ikke løst"
    events: { open: applied } # ONLY when rebuilding a live site: the event names its
                              # aggregates.yaml already has, so its records keep replaying
    attended_by: >            # ONLY for a stage no desk button moves: the automation that does
      what moves it, in a sentence
    max_fields: 4             # decide this BEFORE writing collects, from what the intake
                              # says about the person; a budget that equals the form's size
                              # is counted, not decided, and is reported as such
                              # how many required fields THIS person will tolerate, from what
                              # the intake says about them: someone on a phone with gloves
                              # on gets 3, a first enquiry 4 to 6, a firm's paperwork 5 to 6
    collects:                 # the form; name is snake_case ASCII, type one of
                              # text (default), email, tel, textarea, date, select
      - { name: name }
      - { name: phone, type: tel }
      - { name: problem, type: textarea }
      - { name: urgency, type: select, options: [This week, This month, No rush] }
      - { name: can_do, type: select, multiple: true, options: [Drive, Cook, Carry] }   # any number
      - { name: committee, type: select, options_from: groups }   # the labelled groups above
      - { name: address, optional: true, private: true }          # never on a public page

Rules:

  • One need per (kind of person, thing they want). Work inside the business counts: a role recording or posting something is a need with enters_as. The intake's "what slows us down" usually names one.
  • Groups are the roles the intake names, as snake_case identifiers.
  • The front door opens with does (at most two features, one role each) and shows, then holds at most seven offers: doors plus needs with no door. A door with no need behind it, or a door on a need with enters_as, is refused. A role does speaks for must have a move of its own after sending (moves_by with submitter) or a public view, or it is a spectator.
  • Every need whose form collects an email should tell the person on arrival and on the decisions about them (mails); the plan then needs mail: { from }.
  • Needs that land in the same bucket must have the same stages, done_when, skippable, moves_by, watched_by and handled_by.
  • Every required field is one the business will use to act on the record. If the intake gives no reason to need it, make it optional or leave it out. The required fields must not exceed max_fields.
  • Only select lists options, and it must list them.
  • Everything a person reads (who, wants, about, select options) is in the intake's language. Everything a machine reads (ids, buckets, groups, stages, field names) is plain ASCII: no æ, ø, å.
  • Write no personas.

Task

A business filled in this intake. Write its plan.yaml: every need the intake implies, for the public and for each role inside the business, with the stages read off how the business says things progress, the form each need collects, and how much each person will tolerate.

Intake

An intake as the owner of a small building contractor would fill it

in: plain words, no portal vocabulary. These are the fields the

onboarding site asks for.

business: Nordvik Bygg contact: { name: Ingrid Nordvik, email: ingrid@nordvikbygg.example } language: en

what_we_do: > We are twelve people. We survey old buildings, draw up rehabilitation plans, handle the permit application, and do the building work with our own carpenters plus subcontractors for electrical, plumbing and scaffolding.

who_comes_to_us: > Housing boards and private owners who have a problem with a building and want someone to look at it. Owners who already know what they want built and need drawings, a permit and a price. Trade firms who want to be on our list of subcontractors. Carpenters and apprentices asking if we are hiring.

what_we_keep_track_of: > Enquiries: they come in, we go and look at the building, we send a quote, and then we either win it or lose it. Sometimes it is simply not for us and we say so straight away. Subcontractors: they apply, we check their company registration, trade approval and insurance. Often papers are missing and we have to ask again. Then they are approved or turned down. Job applications: we read them, invite some to an interview, and hire or decline. Things that go wrong on site: a hazard, a near miss, wrong materials delivered. Someone reports it, the safety officer decides what is done about it, and closes it when it is fixed.

who_is_involved: > Two project managers handle enquiries, subcontractors and hiring. One safety officer handles everything reported from site. The crew, ours and the subcontractors' people, are the ones on site who report things. They are on their phones, with gloves on, and will not fill in a long form.

what_slows_us_down: > Enquiries arrive by phone, email and text and get lost. We never know which subcontractors have valid papers. Site problems get mentioned at lunch and forgotten.

Reply with the YAML only, no prose, no code fences.