Rule Engine Setup
The rule engine is what makes Levy Service different from a generic CMMS. Rules watch live telemetry and create tasks automatically. A well-tuned fleet hits more than 50% auto-created tasks within a few weeks of going live.
This article covers the seven trigger types, the JSON config shape, and how to dry-run a rule before turning it on.
Where rules live
Rules are managed at /dashboard/task-rules. Each row in the table shows:
- Name (your label)
- Trigger type (one of the seven below)
- Action summary (what task it creates)
- Last run and last match count
- An Enabled toggle
Rules are stored in the task_rules table and respect RLS — every rule belongs to a subaccount.
The seven trigger types
1. mileage
Fires when a vehicle's odometer crosses a threshold relative to its last close of the same type.
{
"trigger_type": "mileage",
"trigger_config": {
"interval_km": 2000,
"last_task_type": "scheduled_maintenance"
},
"task_type": "scheduled_maintenance",
"default_priority": "medium"
}
The cron at /api/cron/task-rule-mileage evaluates this every hour.
2. time
Calendar cadence. Runs daily at 06:00 UTC.
{
"trigger_type": "time",
"trigger_config": { "every_days": 90 },
"task_type": "inspection",
"default_priority": "low"
}
Pair this with mileage for "every 2000 km OR every 90 days, whichever comes first" by writing two rules.
3. condition_report
Fires when a post-ride condition report scores above an AI severity threshold.
{
"trigger_type": "condition_report",
"trigger_config": { "min_ai_severity": 4 },
"task_type": "repair",
"default_priority": "high"
}
This is an event-style trigger: the rider app's condition_reports insert calls onConditionReportCreated immediately, so the task lands on the board within seconds of the ride ending.
4. low_battery
Battery percentage drops below a threshold AND the vehicle has been parked for a configurable window. Prevents tasks from being created mid-ride.
{
"trigger_type": "low_battery",
"trigger_config": {
"battery_threshold": 20,
"parked_minutes": 360
},
"task_type": "battery_swap",
"default_priority": "medium",
"action_config": { "assign_by_proximity": true }
}
Evaluated by /api/cron/task-rule-low-battery every 30 minutes.
5. rider_issue
A rider submits an issue through the mobile app's ReportIssue flow. Event-style.
{
"trigger_type": "rider_issue",
"trigger_config": { "category": "mechanical" },
"task_type": "repair",
"default_priority": "high"
}
If the rider attaches a photo and the AI rates it severity 4+, priority is bumped to critical regardless of the rule's default.
6. iot_fault
A fault code lands in iot_events. This covers Queclink fault flags, OKAI W0,4 BMS errors, and accelerometer crash events.
{
"trigger_type": "iot_fault",
"trigger_config": { "fault_codes": ["ACCEL_CRASH", "BMS_FAULT"] },
"task_type": "inspection",
"default_priority": "critical"
}
Critical crash events also flip the vehicle to maintenance instantly via the auto-flip trigger.
7. geofence
Vehicle leaves your operating zone for more than N minutes. Creates a retrieve task.
{
"trigger_type": "geofence",
"trigger_config": { "outside_zone_minutes": 30 },
"task_type": "retrieve",
"default_priority": "high",
"action_config": { "assign_by_proximity": true }
}
Evaluated by /api/cron/task-rule-geofence every hour.
Action config
action_config lives next to trigger_config and controls assignment, notifications, and downstream effects.
| Key | Effect |
|---|---|
assign_by_proximity | Pick the nearest available tech based on users.last_lat/last_lng |
assign_role | Constrain proximity to a role, e.g. chargehand for battery_swap |
assign_team_id | Push to a specific team's queue |
change_vehicle_status | Flip vehicle to maintenance even if priority is below high |
notify_slack | Fire a Slack webhook on rule match |
Dry-run before enabling
Every rule has a Test button that calls POST /api/task-rules/[id]/test. The dry-run evaluates the rule against your current fleet and returns the matching vehicles without creating tasks. Use this before flipping a new rule on to confirm it does not match the entire fleet.
For example, if low_battery returns 240 matches on a 250-vehicle fleet, your parked_minutes is probably too short.
Rule loops and deduplication
Rules cannot create the same task twice. The createTaskInternal helper checks for an existing open task of the same task_type against the same vehicle and returns skipped_duplicate: true if found. This is logged in task_rule_runs for audit but does not create a new row.
This prevents the classic loop where a rule fires, the action flips the vehicle to maintenance, the status change re-triggers the rule, and you get 10,000 phantom tasks before lunch.
The audit table
Every rule run is logged in task_rule_runs with the matched vehicles, the created task IDs, and any skipped duplicates. Use this to debug a rule that "isn't firing" — it might be running and matching zero vehicles, or matching them all and being deduped.
Start with three rules, not thirty
The most common mistake is enabling every rule on day one. Start with low_battery, one condition_report rule, and one mileage rule. Add more once you've seen them run cleanly for a week.