Features
Detailed feature reference for oxide-dispatch.
Detailed feature reference for oxide-dispatch.
Real-Time Alert Broadcast
When an alert is created, oxide-dispatch immediately fans it out to every on-duty responder whose framework job matches the alert's job list.
- Job-targeted delivery — Alerts are addressed to one or more job groups (
police,ems,fire) or raw job names (sasp,bcso). Seeshared/config/jobs.lua. - On-duty filtering — When
Config.Broadcast.onDutyOnly = true, dispatch consultsoxide-police(when present) for police-group duty status. Jobs without a duty system fall back to "has the job". - Server-authoritative coords — Client-originated alerts always use the server-side ped coordinates, not coordinates supplied by the client.
- Rate limiting — Player-originated alerts are gated by
Config.Broadcast.rateLimitMs(default 5000ms per player). - Job-cap protection — Resolved jobs are trimmed to
Config.Broadcast.maxJobsPerAlert(default 12) to prevent runaway fan-out.
Responder Lifecycle
Each alert tracks its responders independently. A responder can attach, change status, and detach over the life of an alert; every transition is persisted to dispatch_responders for the audit trail.
| Status | Description |
|---|---|
responding | Responder accepted the alert and is en route |
on_scene | Responder has arrived at the alert location |
cleared | Responder detached from the alert |
- Auto-status — An alert moves from
activetorespondedas soon as one responder is inrespondingoron_scene. It returns toactivewhen all responders are cleared. - Disconnect cleanup — On
playerDroppedandolink:server:playerUnload, every active responder row for that character is auto-detached and markedcleared. - Callsign hydration — When a responder attaches, dispatch reads the
callsigncharacter metadata key and stores it on the responder row so MDTs and replies can reference it without an extra lookup.
Auto-Expiry Sweeper
Old alerts never linger forever. A background sweeper marks unresolved alerts as expired after their TTL elapses.
| Setting | Default | Effect |
|---|---|---|
Config.AutoExpire.enabled | true | Toggles the sweeper |
Config.AutoExpire.defaultMinutes | 15 | TTL applied at create time |
Config.AutoExpire.sweepIntervalSec | 30 | How often the sweeper runs |
Each Alerts.Create call may also override the TTL with a per-alert expireMinutes field.
Persistent MySQL Audit Trail
Every alert and every responder transition is written to MySQL. This is the canonical record — closing the resource and starting it again rehydrates active alerts from the database.
dispatch_alerts— full alert payload, source identity, blip metadata, status, expiry, closure metadata, and replies (JSON column)dispatch_responders—attached_at,detached_at, status, char_id, responder name, job name, optional callsign
Closed/expired alerts remain in the database for reporting, audit, and the heatmap; only active and responded alerts live in the in-memory cache.
Non-Focused Stacked NUI Popup
The client UI is a non-focused popup that does not block player input.
- Stacked rendering — Up to
Config.Popup.maxStacked(default 3) alerts visible simultaneously - Auto-dismiss — Each popup is dismissed after
Config.Popup.durationMsmilliseconds (default 20000) - Mini-map preview — Each popup shows a zoomed map preview at
Config.Popup.mapZoom(default 3) - Sound effect — Plays
Config.Popup.soundEffectfromConfig.Popup.soundSeton receive (defaults:Lose_1stfromGTAO_FM_Events_Soundset). Per-alert overrides are honored. - Live updates —
alertUpdatedevents refresh the displayed responder list in place;alertClosedremoves the popup - World blip — Each alert places a short-range world blip with the configured sprite/color/scale. Panic alerts flash.
Dispatch Hub
A full-screen NUI overview for any responder group. Opened via the Dispatch Hub radial entry.
- Active alerts list — Live feed of alerts addressed to the responder's group, sorted by priority
- Map view — All active alerts pinned on the map
- Heatmap overlay — Toggleable density layer pulled from closed/expired history (see Heatmap)
- Quick actions — Respond, Stop Responding, Set Waypoint, Close
- Configurable radial toggle —
Config.DispatchHub.RadialEnabled(defaulttrue). Disable on servers where another resource (e.g.oxide-policeMDT) already provides a dispatch view, to avoid duplicate radial entries.
The PoliceLookup module always publishes two per-player statebags for downstream consumers: oxide-dispatch:isLEO (boolean) and oxide-dispatch:responderGroup (string|nil, e.g. 'police'/'ems'/'fire'). When oxide-police is running, it supplies the live list of which jobs count as police; otherwise the static lists in jobs.lua are used.
Heatmap
Closed and expired alerts are aggregated into a heatmap surface for the Dispatch Hub.
| Setting | Default | Effect |
|---|---|---|
Config.Heatmap.enabled | true | Toggles the data feed |
Config.Heatmap.defaultWindow | '30d' | Default window. One of '24h', '7d', '30d', 'all' |
Config.Heatmap.cacheSeconds | 60 | TTL of the in-memory cache per window |
Config.Heatmap.maxPoints | 5000 | Max points returned per query (downsampled when exceeded) |
The cache is invalidated whenever an alert closes or expires, so the next request always reflects fresh data within the TTL.
Civilian 911 / 311
Citizens dispatch through three entry points:
Radial menu — Dispatch → Call 911 / Call 311 → category. Fires immediately, no message. (The Call 911 / Call 311 labels are translatable in locales/en.json.)
/911 <message> chat command — Sends a written description.
/311 <message> chat command — Same flow targeted at non-emergency channels.
Categories are configurable in Config.Civilian.Categories911 / Categories311, each mapping to a code, title, and target responder group.
- Anonymous by default —
Config.Civilian.anonymousDefault = truestrips caller identity from the broadcast payload but tracks the caller's source server-side for replies. - Two-way replies — Responders can reply with
/replycaller <alertId> <message>(or via the MDT). The citizen receives a toast prefixed with the responder's callsign / group label. - Server-authoritative coords — Caller location is taken from the server-side ped, never trusted from the client.
- Reply persistence — Replies are stored in the
dispatch_alerts.repliesJSON column for the audit trail.
Panic / Responder Backup
A first-class subsystem distinct from the regular panic dispatch code. Triggered by:
F4keybind (default — rebindable via FiveM keymapping)/panicchat command- The Panic / Backup item in the Dispatch radial menu
| Behavior | Default | Configured by |
|---|---|---|
| Per-responder cooldown | 60 seconds | Config.Panic.cooldownSec |
| Bypasses on-duty filter | true | Config.Panic.broadcastOffDuty |
| Auto-waypoints recipients | true | Config.Panic.autoWaypoint |
| Custom alert sound | inherits popup sound | Config.Panic.soundEffect / soundSet |
| Per-group titles / messages | yes | Config.Panic.titles / messages |
Panic broadcasts to the same responder group only (police panic → police, medic panic → ems, firefighter panic → fire). Title and message strings are per-group so the alert reads naturally for each responder type.
Automatic Crime Detection
Auto-detect runs through the standard alert pipeline (rate limiting + duty filter + persistence apply normally).
| Detector | Code | Default | Trigger |
|---|---|---|---|
| Shooting | 10-71 | enabled | CEventGunShot while on foot (silenced weapons skipped) |
| Drive-by | 10-72 | enabled | CEventGunShot while in a vehicle |
| Melee | 10-34 | enabled | CEventShockingSeenMeleeAction — fires from witnessing NPCs while you're in melee combat, only after an actual hit lands (air punches and missed swings are ignored) |
| Explosion | 10-77 | enabled | CEventExplosionHeard |
| Death | 10-52 | enabled | olink:server:playerDied (auto-routes to EMS) |
| Vehicle theft | 10-30 | enabled | CEventShockingCarAlarm + CEventPedJackingMyVehicle + oxide-vehicles:client:lockpick / :hotwire |
- Per-detector recipient routing — Each detector has a
jobsfield (Config.Detection.<Detector>.jobs) accepting a responder-group key, a raw job name, or an array of either. Tune per-detector to match your server's responder makeup — e.g. setShooting.jobs = { 'police', 'ems' }to pre-stage EMS on shots fired, or changeDeath.jobsto include fire for water/hazard rescue scenarios. - Per-player-per-detector cooldown —
Config.Detection.rateLimitPerPlayerMs(default 8000ms) prevents flooding from continuous fire. Override per detector withConfig.Detection.cooldownsMsto give each one its own timer. - Detection chance — every detector accepts a
chancefield (0.0–1.0, default1.0) controlling how often a qualifying event actually creates an alert, so auto-detection can feel less constant.0.0disables the detector, same asenabled = false. - Server-side duplicate suppression —
Config.Detection.ServerDedupdrops repeated identical auto-detect alerts from the same person within a configurable window, independent of the client cooldown so a spoofed client can't flood dispatch. - Weapon blacklist —
Config.Detection.WeaponBlacklistkeeps less-lethal/training weapons (stungun, flaregun) from triggering alerts. - Zone blacklist —
Config.Detection.ZoneBlacklistexcludes shooting/explosion alerts inside PD ranges or weapon-test zones. - Job blacklist —
Config.Detection.JobBlacklistsilently excludes players whose framework job is on the list, so on-duty responders don't dispatch themselves when they fire their service weapon, ram-and-clamp a vehicle, etc. Manual paths (911, panic, MDT-issued alerts) ignore this list. - Defense in depth — Both client (weapon blacklist) and server (zone + job blacklists) enforce filters; the bridge whitelists payload fields so the server can re-validate.
Per-Group Responder Statuses
Each responder group has its own status list — police-coded labels (Code-4, 10-7) don't leak into EMS/Fire UIs.
| Group | Statuses |
|---|---|
| Police | Available, Busy / On Call, Code-4, 10-7 (Out of Service) |
| EMS | Available, On Call, En Route, On Scene, Transporting, 10-7 |
| Fire | Available, En Route, On Scene, Returning, 10-7 |
_default | Available, Busy, 10-7 |
- Status is in-memory only and resets to default (
Config.ResponderStatusDefault = 'available') on player join. - Status changes broadcast to colleagues in the same group via
olink:client:dispatch:officerStatusChangedso MDTs and dashboards stay in sync. - A group switch (police → ems, etc.) automatically resets the status if the previous value isn't valid in the new group's list.
Statuses are surfaced through the Dispatch radial menu's Set Status submenu (entries vary by group — Officer / Paramedic / Firefighter), olink.dispatch.SetOfficerStatus(statusId), or the oxide-dispatch:server:setOfficerStatus callback.
Callsigns
Responders can set a stable callsign that surfaces on alerts, replies, and status broadcasts.
- Set via radial — Dispatch radial menu → Set Callsign → input dialog
- Set via command —
/callsign <value>(alphanumeric + dashes, max 16 chars) - Persistence — Stored in the character's
callsignmetadata key througholink.character.SetMetadata - Hydration on attach — When a responder attaches to an alert, the callsign is written to
dispatch_responders.callsignso historical rows preserve the value at the time of attach
Accept-Latest Keybind
The G keybind (default — rebindable via FiveM keymapping) instantly:
Marks the local player as responding on the most recent alert addressed to them
Drops a GPS waypoint at the alert's coordinates
Posts a "Responding: [code] title" toast
Silent early-return when there's nothing to accept or when the local player is already on the alert's responder list.
Multi-Framework Support via o-link
All cross-resource calls go through o-link, so the same oxide-dispatch build works on QBCore, QBX, and ESX with no source changes.
olink.character.GetIdentifier(src)— character ID for audit rowsolink.character.GetName(src)— first/last name forsource_nameenrichmentolink.character.GetMetadata(src, 'callsign')— callsign read/writeolink.job.Get(src)— responder's current jobolink.job.GetPlayersWithJob(jobName)— recipient list for broadcastolink.notify.Send(src, msg, type, duration?)— toast notificationsolink.radial.AddItem/RemoveItem— radial menu items (when an o-linkradialprovider is registered)olink:server:playerReady/playerUnload/jobChanged/playerDied— lifecycle hooks
Downstream resources (such as MDT systems) consume dispatch through the olink.dispatch.* namespace — see Exports & Integration.
Legacy Compatibility Shims
oxide-dispatch accepts events from popular legacy police- and ambulance-alert resources so existing scripts keep working without edits.
| Legacy Event | Source | Toggle |
|---|---|---|
police:server:policeAlert | qb-policejob and most QB/QBX drug/robbery scripts | Config.Compatibility['qb-core'].serverPoliceAlert |
police:client:policeAlert (relay) | QB client-side alert callers | Config.Compatibility['qb-core'].clientPoliceAlert |
hospital:server:ambulanceAlert | qb-ambulancejob / qbx_ambulancejob, /911e, third-party | Config.Compatibility['qb-core'].ambulanceAlert |
hospital:server:emergencyAlert | QBX radial menu fast path | Config.Compatibility['qb-core'].emergencyAlert |
hospital:client:ambulanceAlert (relay) | QB client-side ambulance callers | Config.Compatibility['qb-core'].clientAmbulanceAlert |
Mirror → qb-phone:client:addPoliceAlert | qb-phone alert UI | Config.Compatibility['qb-core'].mirrorQbPhone |
esx_policejob:alert / esx_policejob:policeAlert | esx_policejob and ESX scripts | Config.Compatibility['es_extended'].serverPoliceAlert |
esx_outlawalert:policeNotify / esx_outlawalert:outlawNotify | esx_outlawalert | Config.Compatibility['es_extended'].outlawAlert |
esx_phone:send (police contact) | esx_phone | Config.Compatibility['es_extended'].esxPhoneSend |
esx_phone:send (ambulance contact) | esx_phone | Config.Compatibility['es_extended'].esxPhoneEms |
esx_ambulancejob:onPlayerDistress | esx_ambulancejob downed-player keybind | Config.Compatibility['es_extended'].distressAlert |
Each shim:
- Resolves a dispatch code from message content using configurable keyword rules (
keywordCodes) - Falls back to the per-framework
defaultCode(default10-31) when no keyword matches - Applies its own per-source rate limit (
Config.Compatibility[<framework>].rateLimitMs) - (QBCore) Optionally mirrors the alert into
qb-phone:client:addPoliceAlertwhenmirrorQbPhone = true
See the Configuration Reference for the full toggle list.
Dispatch Code Catalog
Every alert carries a code (e.g. 10-71, 10-80, 911). The catalog in shared/config/alerts.lua supplies sensible defaults for any code not explicitly populated by the producer — title, priority, icon, and blip metadata.
Shipped codes
| Code | Title | Priority | Category |
|---|---|---|---|
10-11 | Traffic Stop | 3 | Patrol / routine |
10-14 | Prowler | 3 | Patrol / routine |
10-66 | Suspicious Activity | 3 | Patrol / routine |
10-30 | Grand Theft Auto | 2 | Crime in progress |
10-31 | Crime in Progress | 2 | Crime in progress |
10-34 | Assault in Progress | 2 | Crime in progress |
10-91 | Store Robbery | 2 | Crime in progress |
10-99 | Narcotics Activity | 2 | Crime in progress |
10-50 | Vehicle Accident | 2 | Vehicle |
10-55 | Intoxicated Driver | 2 | Vehicle |
10-57 | Hit and Run | 2 | Vehicle |
10-32 | Person With a Gun | 1 | High priority |
10-71 | Shots Fired | 1 | High priority |
10-72 | Drive-By Shooting | 1 | High priority |
10-77 | Explosion | 1 | High priority |
10-80 | Pursuit | 1 | High priority |
10-90 | Silent Alarm | 1 | High priority |
10-52 | Medical Emergency | 2 | EMS |
10-53 | Person Down | 1 | EMS |
10-54 | Cardiac Arrest | 1 | EMS |
10-56 | Trauma / Injury | 2 | EMS |
10-58 | Overdose | 1 | EMS |
10-70 | Structure Fire | 1 | Fire / hazmat |
10-73 | Vehicle Fire | 1 | Fire / hazmat |
10-74 | Brush / Wildfire | 1 | Fire / hazmat |
10-75 | Hazmat Incident | 1 | Fire / hazmat |
10-76 | Rescue / Extrication | 1 | Fire / hazmat |
10-13 | Officer Needs Backup | 1 | Officer safety |
panic | Panic Button | 1 | Officer safety |
911 | 911 Call | 2 | Citizen-initiated |
311 | 311 Tip | 4 | Citizen-initiated |
Priority 1 is highest. Unknown codes fall back through Config.GetCodeDefaults(code) to a generic Alert with priority 3.
Code defaults
Each entry supplies:
title— human-readable labelpriority—1–4(lower is more urgent)icon— Font Awesome class for the popupblip—{ sprite, color, scale }for the world blip on the responder sidesoundEffect/soundSet(optional) — per-code sound override
Downstream Consumption via olink.dispatch.*
Other resources (MDTs, body cams, CAD systems) read and write dispatch through the o-link namespace:
local olink = exports['o-link']:olink()
local active = olink.dispatch.GetActiveAlerts('police')
olink.dispatch.RespondToAlert(alertId, source)
olink.dispatch.UpdateResponderStatus(alertId, source, 'on_scene')
olink.dispatch.CloseAlert(alertId, source, 'resolved')Full export reference: Exports & Integration.
First-party producers built on top of dispatch include oxide-baitcar, which fires entry and drive-off alerts when a non-officer interacts with a deployed bait car.
AI Radio Dispatcher (optional)
One or more dedicated radio channels staffed by AI dispatchers, one per framework job (department). Officers tune in via their existing radio panel, press PTT, and the AI listens, responds with synthesized voice, and files structured alerts through the regular dispatch pipeline.
- One channel per department — bind each AI dispatch channel to a single framework job (LSPD, BCSO, SASP, AMR EMS, fire, etc.). Each channel's dispatcher self-identifies correctly and routes alerts to that job by default.
- Two provider modes — OpenAI Realtime (sub-second latency, token-priced) or a chained Whisper → GPT-4o-mini → TTS-1 pipeline (~2s latency, dramatically cheaper per call). Set globally or per channel.
- Per-channel overrides — voice, provider, and identifier whitelist can be set per channel.
- 9 or 17 tools — every channel gets the core 9 tools (alert create/cancel/update, responder attach/detach/status, officer status, panic, list alerts). Police-flavored channels (any job in
Config.ResponderGroups.police.jobs) add 8 MDT-backed tools (plate/person/BOLO/custody/dossier) when a supported MDT is running —oxide-police,ps-mdt,wasabi_mdt, orlb-tablet, configured in priority order viaConfig.AIDispatch.Integrations.Mdt. - Spoken acknowledgements — the dispatcher speaks a short "stand by" line ("Running that plate", "Pulling those records") the instant it starts a lookup, so there's no dead air while it works. Phrases are customizable in
shared/config/ai_dispatch_acks.lua. - Comms-radio audio — the synthesized voice is run through a radio filter with light static and mic-open / squelch effects so it sounds like real radio traffic. Tune or disable it under
Config.AIDispatch.RadioFX. - Channel-scoped fan-out — RADIO transcripts and TTS audio stay within the speaker's channel (job). DISPATCH UPDATE notes follow each alert's
jobsrecipient list so cross-department alerts reach every addressed dispatcher. - Identifier whitelist — optional FiveM identifier allowlist (global or per-channel) on top of whatever radio system grants channel access.
- Idle session sweeper — auto-closes forgotten realtime sessions to bound cost.
Inactive until an OpenAI API key is set via the oxide:ai-dispatch:openai_key server convar — without it, the feature loads but join attempts return missing_api_key / missing_openai_key depending on provider mode. Full setup, costs, voice options, and privacy disclosure templates live in the AI Radio Dispatcher guide.