Generated from
cat-harness/skills/process/workflow/bpmn-processes.md— do not edit here.
Processes are BPMN, and the diagrams are executable
The .bpmn file is the source of truth. The rendered SVGs are generated —
regenerate after editing one, and the staleness check fails the build if you
forget. Never hand-edit a rendered diagram.
Do not quote a count of diagrams from prose. This repository’s own
AGENTS.md said “six .bpmn files” long after there were far more, in a file
that elsewhere warns against exactly that. Count the directory.
Authoring: if it has actors, activities and a control flow, it is a process
Author it as BPMN, not as a diagram-in-a-fence. Fenced diagrams stay for the things that are not processes — component maps, an inheritance lattice, a navigation graph.
Every activity carries <bootstrap.processes:skill ref="…"> naming the skill that
implements it, and <cat-harness.processes:bean …> where it touches the work plan. Add both
when you add an activity; the audit reports an activity that names no skill, and
the exemptions for the legitimate cases are declarations, not silence — see
role-model.
Lanes bind roles, not people. A lane is the role; an actor takes it on for
the duration. role-model carries that model.
There is no tool element, and that is a decision
An activity names a skill. A Tool declares satisfies: ["<skill>"]. So the
activity→Tool join already exists and is derivable — activity → skill →
Tool — and a <…:tool ref> on an activity would be a second, independent edge
answering the same question, free to disagree with the first.
It would also put the edge on the wrong side. satisfies is owned by the Tool,
which knows what it implements and is versioned with it; a tool ref would be
owned by the diagram author, who would have to track Tools across every
instance — measured 2026-09-26, 116 Tools are discovered from four
tools/ graphs (cat-harness, the DAK layer, the content core,
the FHIR IG layer), so a diagram in one instance would be asserting facts about
three others.
What is NOT being claimed is that the join is well populated. Measured the
same day over the authoritative registry (tools/discover.ts, never a grep):
distinct skills named by an activity, across all 74 .bpmn | 99 |
| …of those, some Tool satisfies | 32 |
| …of those, no Tool satisfies | 67 |
| distinct skills some Tool satisfies, corpus-wide | 69 |
The gap is real and is reported rather than gated —
activity-skill-has-tool (minor) locates it per activity, and
check:tools carries the corpus-wide count with the ruling that a skill with
no Tool is not an error, since plenty of skills are pure judgement. Those
two are a location and a count, not two answers: a corpus total cannot say
which activity hands a performer a skill whose mechanism is still inlined in
its prose.
One number here disagrees with check:tools, which reports “64 skill(s) have a
Tool” against the 69 above, and the disagreement is recorded rather than
reconciled away: the two count different sets (a satisfies naming a skill
that does not resolve is in one and not the other), and neither is wrong for
its own question.
A Tool’s OWN procedure is the Tool’s subprocess, not the skill’s prose
Owner, 2026-09-30 (placement ruling 6): “in general tools can describe
their own specific subprocesses if needed to not bog down general skills”.
The direction of the edge stays the one above — the Tool owns it. A Tool
whose way of exercising a skill has steps of its own (a retry loop, a staging
directory, a two-pass mode) names that diagram in subprocesses, a list
of process ids on its Tool node; check:tools refuses one no .bpmn in the
checkout has as its stem. The general process stays general and reaches the
detail by calledElement only where it has chosen that Tool. By convention
the diagram sits in the declaring instance’s processes/tools/ concern
group, beside other tools’ procedures rather than among the general
processes.
Processes are grouped by concern, from within
processes/processes.json ("$schema": "concern-groups/v1") names the
groups — codes of code-lists/concern-group.json — and each is
processes/<group>/, a DMN under processes/<group>/decisions/. A higher
instance’s same-named processes/<group>/ is a member of the harness’s group
and declares none of its own. workflowFiles walks recursively, so grouping
moves no diagram out of reach; the rules are in
directory-conventions §”Concern
groups”, checked by check:concern-groups.
A process binds only skills its instance can reach (STRICT)
Owner, 2026-10-03: a process may bind only skills (and, through them, Tools)
from its own instance or one it needs, transitively, “general rule, not
just fhir-harness”. A <bootstrap.processes:skill ref> to a skill that only
a HIGHER instance holds is a wrong-direction edge, the same as an upward
import: the diagram cannot be lifted into its own repository without the layer
above it.
bun run cat check:process-bindings gates it, with the same allowedFromNeeds
direction as check:import-direction and check:reference-direction.
skill-ref-resolves does not catch this, and that is why the gate exists:
it resolves against knownSkills(root), which in a pre-split checkout is
checkout-scoped, so “resolves” means “exists somewhere”. On 2026-10-03 that
hid 33 wrong-direction bindings in 9 diagrams behind a green audit. They are
in a ratchet baseline, cleared under bean mlux.
The two say so about each other (owner, 2026-10-04, option 1 of 3):
skill-ref-resolves is existence only, and its own summary names this gate
as the answer on direction. That leaves one implementation of “may this process
bind this skill”, not two free to disagree.
When it fires there are two fixes, and choosing between them is a placement decision, not a mechanical one:
- move the process up to the instance that holds the skill, when the process is about that layer’s concern;
- move the skill down, when the skill is generic and only happens to live too high.
Never widen the baseline to admit a new binding.
Binding the extension namespaces
An element’s prefix names the Subgraph that declares it (owner, 2026-09-24,
bean 12s9). A diagram binds one address per vocabulary it uses:
xmlns:bootstrap.processes="https://litlfred.github.io/bootstrap/0.1.0/processes/ns#"
xmlns:cat-harness.processes="https://litlfred.github.io/cat-harness/0.1.0/processes/ns#"
bootstrap.processes:forskill,roleandprecondition, which bootstrap declares inbootstrap/processes/ns.jsonld.cat-harness.processes:for every other element (bean,policy,adjudication,raci, …), declared incat-harness/processes/ns.jsonld.
Readers match the address, never the prefix text, so a diagram that binds a
different prefix to the same address reads identically, and folio: bound to
anyone else’s address is not ours. The older single address,
…/folio-assistant/bpmn, is retired (2026-09-24). An element under it is
no longer read, and external-schemas reports a diagram that still binds it
as drift. A test fails if an element is written under an address whose
vocabulary does not define it.
Edge routing: rectilinear, and never over a task
Owner, 2026-09-23, on document-ingestion’s rendered diagram: “keep
rectilinear if possible, no overlapping.” Two rules, and the second is the
one that actually bites.
Rectilinear. A sequence flow turns at right angles. A diagonal reads as a different kind of edge to anybody who has seen a BPMN diagram before, and the notation has no such kind — so the reader spends attention deciding whether the difference means something. It does not.
Never over a task, a lane label or another edge. The case that prompted
this: the gap flow from Record the gap as a bean back to Derive content
from the assets was drawn as one long diagonal crossing the full width of the
process, passing under every task in the lane. It is a perfectly ordinary
loop-back and it was the hardest edge on the page to follow.
A loop-back belongs in the channel below the lane’s tasks — out of the row, back along it, and up into its target. That is where a reader already looks for one, and it crosses nothing.
This is about the DIAGRAM, not the process. BPMNDiagram carries where a
thing was drawn and the process carries what is true, so a routing fix changes
no semantics and needs no re-validation of the flow — the same split
board-diagram-interchange draws one level up. What it changes is whether
somebody can read the thing.
Check it by looking at the rendered SVG, not the XML. Waypoints that look
orderly in source can still emit a diagonal, and render:bpmn is what a reader
sees. The rendered page is the artefact under review — preview:site exists
for exactly this reason, after a generator shipped 22 headings with one anchor
and every gate was green across it.
Running one: the engine refuses work claimed out of order
The list / start / next / complete calls run a process from its diagram. “What is enabled now” is the useful one: it reports the enabled step, the lane that owns it, and the skill that implements it — so the answer is something to act on rather than a bare step name.
Completion refuses a step that is not enabled. That is what makes the diagram a control rather than a picture.
Completion also checks WHO. Before any task or decision is recorded, the
engine asks four questions: is the actor authenticated, eligible for the role
the lane binds (roleRef), permitted by an ODRL policy to perform-task in
this process and task, and allowed to touch the target content? This is
generic engine behaviour, so do not draw an authorization task into a
diagram: a check drawn into some processes is a check missing from the rest.
What a diagram owes the check is a lane bound to a declared role. See
task-authorization.
Instance state is committed, alongside the work plan, so a sibling session sees the same position. That is the whole reason not to hand-roll a second tracker: a second answer to where are we is free to disagree with the first.
Strict by default, and what a package may never relax
Base processes carry <cat-harness.processes:policy enforcement="strict"/> — the gate
refuses a step that is not enabled. Per-content-type processes are advisory:
their package owns what adequate means in that domain. Absent policy means
strict.
To relax a base step, a package declares it with a reason. No reason, no load — silencing a gate must cost more than satisfying it.
Some steps are marked unrelaxable and no package may touch them: the editor seeing the findings, the decision itself, the write, and release authorisation. Those four are the gate; everything else is procedure around it.
A validator checks every declared relaxation and runs in CI, so a relaxation naming a step that no longer exists fails rather than sitting in the file looking like policy while permitting nothing.
The commit boundary enforces what the diagram decided
A corpus gate, run from a pre-commit hook or CI in a folio, refuses a changed block that no instance records the editor having authorised — no instance, not past the decision, or discarded.
It refuses when it cannot tell, too. A file that reads as a manifest but will not import is refused rather than waved through: could not determine is never rendered as a pass. Sweep-written sidecars are excluded, and a warn-only mode exists for gradual adoption.
Some gateways are computed, not chosen
A gateway carrying <cat-harness.processes:decision/> is backed by a decision table. Pass
the facts — the counts a QA sweep already produced — and the table returns the
branch.
Completion refuses a hand-supplied outcome there, and that refusal is the point: asserting the answer would defeat having a table. Adding one means adding the table and the reference and nothing else; the loader checks that every outcome the table can return names a real branch.
A bean-marked step performs the operation, not a note about it
An activity carrying a bean operation does it when you complete the step:
| op | what happens |
|---|---|
claim | sets in-progress, idempotent |
note | appends what you pass |
resolve | completes the bean only once the instance itself has completed |
That last row is a judgement guard, not a sequencing detail. A still-running process gets a note instead, because whether work is done is a judgement and a bean is never closed on someone else’s say-so — the same rule that stops you resolving a sibling’s.
There is no fourth op, and resolve is NOT the end of the bean’s life. resolve sets completed; completed is what beans archive moves; and
nothing in any diagram archives. Measured 2026-09-20: draft-to-publication
does claim → note → resolve, crdm-close does resolve, code-change-review
does claim → resolve — every one manufactures the condition and none
discharges it, which is how 219 beans accumulated.
Before reaching for an archive op, read
todo-manager §”Archiving — two dispositions”: an op and
a periodic sweep answer different questions, and for most processes the
answer is the sweep. An op is worth an edge on your diagram only where your
process’s completion is itself the reason a bean is finished — and then the
archive records why, which a sweep cannot. Bean folio-assistant-m8gz
decides which this instance builds.
Work-plan priming reports every instance’s position next to its bean, so the plan and the process are one answer rather than two.