FEI Entry Screening - AI
An extension that reads the free-text comments attached to entries after an Equipe import, and turns them into block lists inside the show — plus a set of age-eligibility lists built from the FEI rules.
It runs on its own, on Equipe's imports webhook — the one that fires when
FEI entries are pulled into the show. Nobody has to launch it. A modal screen
inside Equipe shows what it could not decide, so a human can finish the job.
Table of Contents
- Overview
- Key Features
- What It Writes
- Height Screening
- Age Blocking
- The Report
- The Review Screen
- Running It
- Safety Rules
- Troubleshooting
Overview
When entries are imported into Equipe, riders and horses arrive carrying free comments written by declarants — in any language, in any style.
Nobody reads six hundred of those by hand. This extension does, using a local AI model running on the organiser's own network — no data leaves the site.
Separately, and without the AI, it checks every entered rider's age against the FEI eligibility rules and builds the lists of riders who may not ride certain classes.
Both kinds of output land in Equipe as lists (the L marker), never as
classes. The organiser attaches them to the classes they belong to.
Key Features
- ✅ Runs by itself when FEI entries are imported — no button to press
- ✅ Reads comments on horses and on riders, in any language
- ✅ Local AI (Ollama) — nothing sent outside the organiser's network
- ✅ Height restrictions → one list per level + height cap
- ✅ Age eligibility → one list per level + age threshold, straight from the FEI rules
- ✅ Multi-week shows → separate lists per week (
W1,W2…), each with its own riders - ✅ Writes nothing on doubt — every uncertain case is reported instead
- ✅ Re-running is safe: existing lists are read first, only what is missing is added
- ✅ A dry run computes everything and writes nothing, so you see the result first
- ✅ A real run is undoable in Equipe in one action
- ✅ Review screen inside Equipe to place the leftover cases by hand
- ✅ Cases that will never matter can be set aside — and come back if the comment changes
What It Writes
Equipe lists — the same object a masterlist is, marked L. Each one gets:
- a stable identifier, so a second import updates it instead of duplicating it
- a short code (
clabb) such asCSI1*-U14orW2CSI2*-U18 - a full name saying what the block is: CSI1* - under 14 years - 1m30 block
- the date of the masterlist it came from, so it lands inside the show week
Nothing else in the show is touched. No class is modified, no entry is removed.
All the writes of one run share a single transaction id, so Equipe can undo the whole run in one action. That undo lives in Equipe, not here: the extension has no revert button of its own.
Its own guard-rail sits earlier, before anything is written: a dry run computes everything, names every list it would create and every rider it would add, and writes nothing. That is the moment to look, and it can be switched on for a show on request.
Height Screening
The AI looks for a sentence saying the rider is approved only up to a certain fence height, at a certain level — in whatever language it was written.
Result: one list per (level, height) pair — for example
block-csi3star-145, holding every rider approved only up to 1.45 m in CSI3*.
A second, model-independent check reads every comment for a plausible height. If the AI says "no restriction" but a height is sitting there in the text, the case is flagged as a disagreement in the report rather than dropped. A missed height restriction is a regulatory problem, not a display bug.
Comments containing no digit at all are set aside before the AI is called — a height cap always contains a number. They are still listed in the report.
Age Blocking
This part never calls the AI. It is arithmetic, exact and checkable, and it runs whatever the AI is doing.
It runs on every import whether it is switched on or not: the survey and the coverage figures are always produced. Only the writing of lists is optional.
How ages are read
The date of birth lives in the person_no field of people.json, which is a
free text field. Accepted: 1962, 1995-10-19, 19/10/1995, 19.10.1995.
Everything else is refused — an FEI ID, a passport number, a future year, an age over a hundred. Mistaking a licence number for a date would produce an absurd block that nobody would question before the paddock.
Age is the FEI age: the age reached during the calendar year, whatever the birthday. A rider born in 2012 is 14 for the whole of 2026, so they leave the "under 14" list on 1 January.
The FEI table
Straight from FEI Jumping Rules 2026, article 205. Each threshold in the report cites the paragraph it comes from.
Each column below is one list. Read a row left to right: it is the schedule table turned into ages that are blocked, not ages that are allowed.
| Level | not eligible | max 1m30 | max 1m40 | all classes | under 18 also barred from |
|---|---|---|---|---|---|
| CSI1* | under 12 | 12-13 | — | from 14 | Power and Skill / Derby |
| CSI2* | under 12 | 12-13 | 14-15 | from 16 | Power and Skill / Derby |
| CSI3*–5* | under 12 | 12-13 | 14-15 | from 16 | GP / Derby / Power and Skill / highest prize money |
| CSIO1*–5* | under 12 | 12-13 | 14-15 | from 16 | GP / Nations Cup / World Cup / Derby / Power and Skill / highest prize money |
| CSIYH | under 12 | 12-13 | 14-15 | from 16 | Power and Skill / Derby |
| CSIAm | under 12 | 12-13 | — | from 14 | Power and Skill / Derby |
So a CSI2* rider aged 12 or 13 is capped at 1m30, aged 14 or 15 at 1m40, and from 16 rides everything — the Grand Prix included. The last column is separate: it applies to every rider under 18, whatever their height band.
The youth categories are not thresholds but windows. Outside them a rider is not eligible at all, whether too young or too old.
| Category | Eligible |
|---|---|
| Juniors | 14 to 18 |
| Young Riders | 16 to 21 |
| Children | 12 to 14 |
| Pony Riders | 12 to 16 |
| U25 | 16 to 25 — and from 14 if the first round stays at 1m40 |
| Veterans | 45 and over |
Two points that are easy to get wrong:
- CSI1* has no threshold at 16. From the year they turn 14, everything at a CSI1* is open to them.
- Power and Skill and Derby are barred to under-18s at every level, amateur classes included. The Grand Prix is only barred from CSI3* upwards and at CSIO.
Height caps apply to the first round, not to the advertised height of the class. A 1m40 class whose first round is at 1m30 is open to a thirteen-year-old.
And a height cap covers every class above it, the Grand Prix included. Below 16 there is no Grand Prix rule at all — only the height counts. So a CSI2* Grand Prix at 1m45 is closed to a fourteen-year-old by the 1m40 cap, and open to a sixteen-year-old, who is only barred from the Power and Skill and the Derby. Two different lists close the same class for two different reasons, and neither of their names mentions it: attach the height lists to every class above their cap.
Youth categories are windows, not floors. A twenty-year-old entered in a Juniors class is as ineligible as a thirteen-year-old, and gets his own list — that is the case nobody notices, because a rider who is too young stands out and a rider who is too old does not.
For those, the list means this rider should not be entered here at all, not this rider is capped at some height.
Shows spanning several weeks
A show covering two FEI events is two weeks, and each week gets its own lists,
labelled W1, W2… The weeks are ordered by date, not by FEI number —
those do not run in order.
Each week's list holds only that week's starters. The same level in two weeks gives two lists with two different populations. A single-week show gets no label at all, to keep the names short.
Coverage — the part that matters
A rider whose date of birth cannot be read will not be blocked anywhere. That is a silent miss, and it is the only real danger in this whole feature.
The overall percentage tells you nothing useful. What counts is the percentage per masterlist, and the report gives it, with the entry numbers of every rider whose age is unknown.
Seen in production: a masterlist reporting 0 minors while a third of its starters had no date of birth — an entire delegation entered without one. The zero meant nothing.
An optional guard can make a masterlist refuse to produce its lists below a chosen percentage of known dates of birth, rather than offering partial protection that looks complete. Off by default: it is the organiser's call between nothing rather than half and half plus the list of blind spots, and it is set per show on request.
The Report
Every run writes a timestamped JSON report, whether or not anything was written. It is the record of what the extension actually saw, not what it concluded.
It contains:
- every list it would build or did build, with its riders and the comment that justified each one
- every case it refused to decide, with the reason in plain words
- every comment set aside by the pre-filter
- the age survey: how many riders have a usable date of birth, the age histogram, the minors, and the ones it could not read
- the coverage per masterlist
- every masterlist it found, and for those it ignored, why
The rule throughout: nothing disappears without a line saying so.
The Review Screen
Opened as a modal from inside Equipe. It reads the latest report and shows a table of everything left to decide:
| Column | Meaning |
|---|---|
| Type | disagreement with the model, unresolved case, or a rider note |
| Rider | who it concerns, with their entry number |
| Comment | the original text |
| Why | why the extension did not act on it |
| Add to | a list to drop them into, one click — or Ignore |
The button greys out to Already in when the rider is already in the list you picked — membership is re-read live, not taken from the report.
Age lists are shown with their counts but cannot be picked: they are computed from dates of birth, and adding someone by hand would make them lie.
A manual entry goes in under the same identifier the extension uses, so a later import will not duplicate it — but will not remove it either.
Setting a case aside
Some comments will never become a list entry — a horse's name typed in the wrong field, a request for electricity, a note to the office. They would come back at every import and eventually bury the cases that matter.
Ignore moves one out of the table. It reappears in a Set aside section underneath, with the date, and a Bring back button. Nothing is deleted and nothing is hidden: a case you cannot see any more is a case you cannot check.
The extension remembers the rider and the text. If the comment is edited in Equipe, the case comes back on its own — a different text is a different request, and it deserves to be looked at again.
Set-aside cases are per show. Ignoring one here does not silence the same comment elsewhere.
Running It
Nothing to launch. The extension is wired to Equipe's imports webhook, the
one that fires when FEI entries are pulled into the show — so it sees riders and
horses at the moment they arrive, comments included, before anyone has opened
the declaration screen.
Any other event Equipe might send is answered and ignored: the extension only acts on an entries import.
When it fires, it:
- answers Equipe immediately, then keeps working in the background — otherwise Equipe times out and reports the webhook as failed
- takes a lock, so two imports in a row cannot both write at once
- reads the show, checks the AI is reachable, analyses, and writes
- leaves a report
Analysis of a full show takes a couple of minutes. Verdicts are cached, so a second import after a small change costs almost nothing — and since existing lists are read before writing, importing twice adds nothing the second time.
Each import refreshes the picture: riders who arrived in the meantime are added to the lists they belong to.
Safety Rules
These are deliberate, and they are why the extension can be left alone:
- Nothing is written on doubt. An uncertain case goes to the report and to the review screen, never into Equipe.
- Existing lists are read before writing. Only missing riders are added. Running the same import twice changes nothing the second time.
- Nothing is ever removed. The extension adds; it does not take away.
- A run can be undone in Equipe in one action, through the transaction id its writes share.
- Nothing existing is modified. The only new objects are the lists themselves; deleting one in Equipe removes everything that run added to it.
- The lists it creates cannot become its own input. They carry no FEI identifier, so they are never mistaken for masterlists on the next run.
- A level it does not recognise produces nothing, and says so, rather than guessing with rules meant for another category.
Troubleshooting
Everything here is checked from Equipe and from the review screen. Anything not listed goes to the administrator with the show number and the time of the import — that is all it takes to find the run.
- No list appeared after an import → open the review screen. If it says the report is for another show, the extension has not run on this one yet: run the entries import again. If the report is the right one and there are still no lists, nothing matched — which is a real answer, not a failure.
- The review screen says there is no report at all → the extension has never run since it was installed. Run an entries import, then reopen it.
- A rider you expected is in no age list → look at their record in Equipe. Without a readable date of birth the extension cannot judge their age, and it will not guess. This is the one gap worth checking by hand before the show.
- A list exists but is empty → note its name and pass it on; that one needs the administrator.
- A case in the review screen keeps coming back after Ignore → its comment was edited in Equipe. That is deliberate: a different text is a different request, and it deserves a fresh look.
- The AI could not read a comment → it is waiting for you in the review screen with the reason. Add the rider to a list, or set the case aside. Nothing is ever written from a guess.
- A list is wrong and you want it gone → delete it in Equipe. It is an ordinary list; deleting it takes its contents with it, and nothing else in the show was touched. The next import will rebuild it from the current data.
- The webhook is reported as failed in Equipe → the extension answers in a few milliseconds, so a failure almost always means the request never reached it. Nothing you can fix from the show: pass it on with the show number.