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). See shared/config/jobs.lua.
  • On-duty filtering — When Config.Broadcast.onDutyOnly = true, dispatch consults oxide-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.

StatusDescription
respondingResponder accepted the alert and is en route
on_sceneResponder has arrived at the alert location
clearedResponder detached from the alert
  • Auto-status — An alert moves from active to responded as soon as one responder is in responding or on_scene. It returns to active when all responders are cleared.
  • Disconnect cleanup — On playerDropped and olink:server:playerUnload, every active responder row for that character is auto-detached and marked cleared.
  • Callsign hydration — When a responder attaches, dispatch reads the callsign character 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.

SettingDefaultEffect
Config.AutoExpire.enabledtrueToggles the sweeper
Config.AutoExpire.defaultMinutes15TTL applied at create time
Config.AutoExpire.sweepIntervalSec30How 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_respondersattached_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.durationMs milliseconds (default 20000)
  • Mini-map preview — Each popup shows a zoomed map preview at Config.Popup.mapZoom (default 3)
  • Sound effect — Plays Config.Popup.soundEffect from Config.Popup.soundSet on receive (defaults: Lose_1st from GTAO_FM_Events_Soundset). Per-alert overrides are honored.
  • Live updatesalertUpdated events refresh the displayed responder list in place; alertClosed removes 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 toggleConfig.DispatchHub.RadialEnabled (default true). Disable on servers where another resource (e.g. oxide-police MDT) 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.

SettingDefaultEffect
Config.Heatmap.enabledtrueToggles the data feed
Config.Heatmap.defaultWindow'30d'Default window. One of '24h', '7d', '30d', 'all'
Config.Heatmap.cacheSeconds60TTL of the in-memory cache per window
Config.Heatmap.maxPoints5000Max 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 defaultConfig.Civilian.anonymousDefault = true strips 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.replies JSON column for the audit trail.

Panic / Responder Backup

A first-class subsystem distinct from the regular panic dispatch code. Triggered by:

  • F4 keybind (default — rebindable via FiveM keymapping)
  • /panic chat command
  • The Panic / Backup item in the Dispatch radial menu
BehaviorDefaultConfigured by
Per-responder cooldown60 secondsConfig.Panic.cooldownSec
Bypasses on-duty filtertrueConfig.Panic.broadcastOffDuty
Auto-waypoints recipientstrueConfig.Panic.autoWaypoint
Custom alert soundinherits popup soundConfig.Panic.soundEffect / soundSet
Per-group titles / messagesyesConfig.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).

DetectorCodeDefaultTrigger
Shooting10-71enabledCEventGunShot while on foot (silenced weapons skipped)
Drive-by10-72enabledCEventGunShot while in a vehicle
Melee10-34enabledCEventShockingSeenMeleeAction — fires from witnessing NPCs while you're in melee combat, only after an actual hit lands (air punches and missed swings are ignored)
Explosion10-77enabledCEventExplosionHeard
Death10-52enabledolink:server:playerDied (auto-routes to EMS)
Vehicle theft10-30enabledCEventShockingCarAlarm + CEventPedJackingMyVehicle + oxide-vehicles:client:lockpick / :hotwire
  • Per-detector recipient routing — Each detector has a jobs field (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. set Shooting.jobs = { 'police', 'ems' } to pre-stage EMS on shots fired, or change Death.jobs to include fire for water/hazard rescue scenarios.
  • Per-player-per-detector cooldownConfig.Detection.rateLimitPerPlayerMs (default 8000ms) prevents flooding from continuous fire. Override per detector with Config.Detection.cooldownsMs to give each one its own timer.
  • Detection chance — every detector accepts a chance field (0.01.0, default 1.0) controlling how often a qualifying event actually creates an alert, so auto-detection can feel less constant. 0.0 disables the detector, same as enabled = false.
  • Server-side duplicate suppressionConfig.Detection.ServerDedup drops 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 blacklistConfig.Detection.WeaponBlacklist keeps less-lethal/training weapons (stungun, flaregun) from triggering alerts.
  • Zone blacklistConfig.Detection.ZoneBlacklist excludes shooting/explosion alerts inside PD ranges or weapon-test zones.
  • Job blacklistConfig.Detection.JobBlacklist silently 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.

GroupStatuses
PoliceAvailable, Busy / On Call, Code-4, 10-7 (Out of Service)
EMSAvailable, On Call, En Route, On Scene, Transporting, 10-7
FireAvailable, En Route, On Scene, Returning, 10-7
_defaultAvailable, 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:officerStatusChanged so 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 callsign metadata key through olink.character.SetMetadata
  • Hydration on attach — When a responder attaches to an alert, the callsign is written to dispatch_responders.callsign so 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.

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 rows
  • olink.character.GetName(src) — first/last name for source_name enrichment
  • olink.character.GetMetadata(src, 'callsign') — callsign read/write
  • olink.job.Get(src) — responder's current job
  • olink.job.GetPlayersWithJob(jobName) — recipient list for broadcast
  • olink.notify.Send(src, msg, type, duration?) — toast notifications
  • olink.radial.AddItem / RemoveItem — radial menu items (when an o-link radial provider 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 EventSourceToggle
police:server:policeAlertqb-policejob and most QB/QBX drug/robbery scriptsConfig.Compatibility['qb-core'].serverPoliceAlert
police:client:policeAlert (relay)QB client-side alert callersConfig.Compatibility['qb-core'].clientPoliceAlert
hospital:server:ambulanceAlertqb-ambulancejob / qbx_ambulancejob, /911e, third-partyConfig.Compatibility['qb-core'].ambulanceAlert
hospital:server:emergencyAlertQBX radial menu fast pathConfig.Compatibility['qb-core'].emergencyAlert
hospital:client:ambulanceAlert (relay)QB client-side ambulance callersConfig.Compatibility['qb-core'].clientAmbulanceAlert
Mirror → qb-phone:client:addPoliceAlertqb-phone alert UIConfig.Compatibility['qb-core'].mirrorQbPhone
esx_policejob:alert / esx_policejob:policeAlertesx_policejob and ESX scriptsConfig.Compatibility['es_extended'].serverPoliceAlert
esx_outlawalert:policeNotify / esx_outlawalert:outlawNotifyesx_outlawalertConfig.Compatibility['es_extended'].outlawAlert
esx_phone:send (police contact)esx_phoneConfig.Compatibility['es_extended'].esxPhoneSend
esx_phone:send (ambulance contact)esx_phoneConfig.Compatibility['es_extended'].esxPhoneEms
esx_ambulancejob:onPlayerDistressesx_ambulancejob downed-player keybindConfig.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 (default 10-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:addPoliceAlert when mirrorQbPhone = 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

CodeTitlePriorityCategory
10-11Traffic Stop3Patrol / routine
10-14Prowler3Patrol / routine
10-66Suspicious Activity3Patrol / routine
10-30Grand Theft Auto2Crime in progress
10-31Crime in Progress2Crime in progress
10-34Assault in Progress2Crime in progress
10-91Store Robbery2Crime in progress
10-99Narcotics Activity2Crime in progress
10-50Vehicle Accident2Vehicle
10-55Intoxicated Driver2Vehicle
10-57Hit and Run2Vehicle
10-32Person With a Gun1High priority
10-71Shots Fired1High priority
10-72Drive-By Shooting1High priority
10-77Explosion1High priority
10-80Pursuit1High priority
10-90Silent Alarm1High priority
10-52Medical Emergency2EMS
10-53Person Down1EMS
10-54Cardiac Arrest1EMS
10-56Trauma / Injury2EMS
10-58Overdose1EMS
10-70Structure Fire1Fire / hazmat
10-73Vehicle Fire1Fire / hazmat
10-74Brush / Wildfire1Fire / hazmat
10-75Hazmat Incident1Fire / hazmat
10-76Rescue / Extrication1Fire / hazmat
10-13Officer Needs Backup1Officer safety
panicPanic Button1Officer safety
911911 Call2Citizen-initiated
311311 Tip4Citizen-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 label
  • priority14 (lower is more urgent)
  • icon — Font Awesome class for the popup
  • blip{ sprite, color, scale } for the world blip on the responder side
  • soundEffect / 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, or lb-tablet, configured in priority order via Config.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 jobs recipient 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.

Next Steps