Issue Rules
Issue Rules turn incoming race events (laps, flags, fuel samples, weather samples) into auto-created board cards so the race engineer doesn't have to manually log "yellow flag, lap 14" or "rain detected, switch to wets?" while juggling the rest of the stand.
Each rule pairs an event type + a condition with an
action template. When the condition matches, the action runs
— today the only action type is card, which drops a row on the
Jobs & Issues board.
Source:
- API route:
apps/api/src/routes/issueRules.ts - Schema:
issue_rulestable inapps/api/src/db/schema.ts
See API → /issue-rules
for the wire shape and
Swimlanes for the lane vocabulary the actions
target.
Rule shape
A rule row carries:
| Field | Type | Notes |
|---|---|---|
name | text | Human-readable label shown in the admin grid |
description | text? | Hover-help / changelog free-form text |
eventType | enum | lap · flag · fuel · weather |
conditions | jsonb | { kind, … } — see the catalogue below |
action | jsonb | { type: "card", swimlane, priority, titleTemplate, bodyTemplate? } |
enabled | bool | Disable without deleting |
lastFiredAt, fireCount | timestamps + counter | Tuning aid in the admin UI |
Supported event types
| Event type | Fires on | Typical condition kinds |
|---|---|---|
lap | Every new /laps row | lapTimeDeltaMs, pitIn, fuelKgBelow, trackStatus |
flag | Track-status change between consecutive laps | trackStatus |
fuel | Per-lap fuel sample (decreasing or increasing direction) | fuelKgBelow |
weather | Every /weather insert | rainStart, trackTempBelowC, trackTempAboveC |
The eventType doesn't restrict which kind you can pair it
with — it's a hint to the runtime about which inserts should
re-evaluate the rule. Pick the type that matches the data
producer.
Condition kinds
Each conditions blob has a discriminator field kind. The
value-bearing fields after that depend on the kind.
kind | Extra fields | Meaning |
|---|---|---|
trackStatus | value ("green", "yellow", "red", "sc", "vsc", "fcy") | Lap or flag-change carries the matching track status |
lapTimeDeltaMs | op (">=", "<=", ">", "<"), value (ms) | Current lap time vs. previous lap, in milliseconds |
fuelKgBelow | value (kg) | Fuel-remaining drops below value |
pitIn | (none) | Lap is flagged pitIn: true |
rainStart | (none) | First weather sample where skyStatus (or precipitation flag) indicates rain |
trackTempBelowC | value (°C) | Weather sample with trackTempC < value |
trackTempAboveC | value (°C) | Weather sample with trackTempC > value |
Conditions are stored as raw JSON, so adding a new kind is a
producer-side change — the route accepts arbitrary objects under
conditions.
Action templating
The action block is currently hardcoded to type: "card". When
the rule fires it inserts a board_cards row with:
| Card field | Source |
|---|---|
title | titleTemplate rendered with the event payload |
body | bodyTemplate rendered with the event payload |
swimlane | action.swimlane (matches a row in /swimlanes) |
priority | action.priority (low · normal · high · critical) |
status | Always todo for rule-created cards |
Templates use the {{token}} syntax. Tokens are resolved
against the event payload that triggered the rule. Common tokens:
{{lapNumber}}— current lap (lap / flag / fuel events){{lapTimeMs}}— current lap's time in milliseconds{{deltaMs}}— lap-time delta to previous lap (only whenlapTimeDeltaMsmatched){{trackStatus}}—"green" | "yellow" | …{{fuelRemainingKg}}— fuel chain output (when computed){{airTempC}}/{{trackTempC}}— weather sample values
Tokens that aren't present on the event payload render as their
literal text (e.g. {{lapNumber}} on a weather event).
The four seeded rules
make seed installs these rules so a freshly seeded account has
end-to-end coverage out of the box. They live in
apps/api/src/db/seed.ts and are good copy-paste templates:
| Name | Event | Condition | Action |
|---|---|---|---|
| Yellow flag detected | flag | { kind: "trackStatus", value: "yellow" } | Strategy / high — title Yellow flag on lap {{lapNumber}}, body warns about pit-window shrinking |
| Lap time spike (+2s) | lap | { kind: "lapTimeDeltaMs", op: ">=", value: 2000 } | Engineering / high — title Lap {{lapNumber}} spike — investigate, asks for telemetry / off-track / driver request |
| Pit-in event | lap | { kind: "pitIn" } | Mechanical / normal — title Pit stop after lap {{lapNumber}} |
| Rain start | weather | { kind: "rainStart" } | Strategy / critical — title Rain detected — switch to wets?, body notes track temp + tyre swap window |
Disable any of them in the admin UI without deleting (sets
enabled: false). Re-enable later without losing the
configuration.
Authoring a rule
/admin/issue-rules is the in-app surface (route registered
under the Admin section once the editor lands; until then,
author rules via direct API calls). The wire shape is:
curl -X POST http://localhost:6161/issue-rules \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Critical fuel low",
"eventType": "fuel",
"conditions": { "kind": "fuelKgBelow", "value": 5 },
"action": {
"type": "card",
"swimlane": "Strategy",
"priority": "critical",
"titleTemplate": "Fuel critical on lap {{lapNumber}}",
"bodyTemplate": "Fuel chain reports {{fuelRemainingKg}} kg remaining. Recommend pit-in next opportunity."
}
}'
What's coming
- Custom action types — beyond
card, future actions could fire a webhook, send an email, or open an issue against a workflow in Issue Workflows. - Compound conditions —
AND/ORof multiplekindrules. Today each rule has one condition. - Deduplication window — repeated firings on consecutive laps produce N cards. A future enhancement would let rules declare a cool-down (e.g. "don't re-fire for 5 laps").
- Rule simulator — replay a session's lap / flag / weather feed against a rule to see how often it fires before enabling in production.