Hooks
Hooks let your resource block, rewrite or react to MDT actions, no matter where they come from: the tablet, the web portal, a server export or an automatic job.
local hookId = exports['koja-mdt']:registerHook(name, function(payload, src)
end, { stage = 'before', priority = 0 })| Option | Default | Notes |
|---|---|---|
stage | 'after' | 'before' runs before the action is saved and can cancel or change it. 'after' runs once it is done. |
priority | 0 | Lower runs first. Equal priorities run in registration order. |
registerHook returns the hook id, or nil, 'unknown_hook' / nil, 'invalid_function'. removeHook(hookId) removes it (only the resource that registered it can). Hooks are removed automatically when your resource stops. GetHookNames() lists every name.
Semantics
-
Before hooks
- Return
falseto cancel. Return a reason as the second value to show the officer a specific error:return false, 'over_limit'(64 chars max). The first hook that cancels wins and later hooks do not run. - Return a table to change the payload. Its keys are merged into the payload and passed to the next hook. Only the fields marked modifiable below are applied; everything else is informational. Values are validated again after your hook (lengths, ranges, enums).
- Payloads are copies when they cross resources, so changing
payloadin place has no effect. Return the changes. - Returning nothing (or
true) lets the action continue unchanged. - Errors are caught and printed to the console; a failing hook never blocks the MDT.
- Return
-
After hooks
After hooks run asynchronously once the action is saved. Return values are ignored. Each after stage is also fired as a server event, see the event bridge below. -
Arguments
srcis the acting player's server id, ornilfor web portal, export and system actions. Most payloads carryby(orofficer/unit):{ source, identifier, name, callsign, department, system }.system = truemarks System and export actions.
Event bridge
Every after stage is also triggered as a server-local event named koja-mdt:<hook> with (payload, src). Use it if you prefer events over hooks. It cannot cancel anything.
AddEventHandler('koja-mdt:warrant:approve', function(payload, src)
print(('warrant #%d approved by %s'):format(payload.id, payload.by and payload.by.name or 'System'))
end)Use AddEventHandler, never RegisterNetEvent: clients cannot trigger these.
Dispatch
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
dispatch:create | A call is created (911, panic, ALPR, exports, tablet). | before: the new call { code, title, message, coords, street, priority, caller, departments = { lspd = true }, blip, status, units, notes, createdAt }; after: the public call. | Cancel only. Modifiable: title, message, priority, departments, blip, street, coords, caller. |
dispatch:update | UpdateDispatch edits a call. | before { callId, title, message, priority, street, coords, code, by }; after { callId, call, by }. | Cancel with reason. Modifiable: title, message, priority, street, coords, code. |
dispatch:note | A note is added (tablet or AddDispatchNote). | before { callId, text, author, by }; after adds call. | Cancel with reason. Modifiable: text. |
dispatch:assign | A unit attaches or is assigned to a call. | { callId, source, unit }, after adds call. | Cancel only. |
dispatch:detach | A unit leaves a call. | { callId, source, unit }. | After only. |
dispatch:close | A call is closed. | { callId, call, by }. | Cancel only. |
dispatch:panic | An officer presses panic. | before { source, officer, street }; after adds callId, call. | Cancel only. |
unit:status | A unit changes status. | { source, status, previous, unit }. | Cancel with reason. |
exports['koja-mdt']:registerHook('dispatch:create', function(call)
if call.code == '10-90' then return { priority = 1, message = (call.message or '') .. ' [bank]' } end
end, { stage = 'before' })Records
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
incident:create | A report is created. | before { title, type, status, location, narrative, tags, images, restricted, department, by } (tags / images are JSON strings, restricted is 0 / 1); after { id, incident, by }. | Cancel with reason. Modifiable: title, type, status, location, narrative, restricted. |
incident:update | A report is saved again (tablet, UpdateIncident, AddPersonToIncident, …). AddChargeToIncident fires the after stage only. | Same as incident:create, plus id. | Same as incident:create. |
incident:delete | A report is deleted. | { id, incident, by }. | Cancel with reason. |
incident:process | A suspect is processed (fine / jail). | { incidentId, personId, identifier, name, target, fine, months, applyFine, applyJail, by }. | Cancel with reason. Modifiable: fine, months, applyFine, applyJail. |
warrant:create | A warrant is requested or issued. | before { type, identifier, name, plate, reason, charges, hours, status, department, by }; after { warrant, by }. | Cancel with reason. Modifiable: reason, hours. |
warrant:approve | A judge approves a pending warrant. | { id, warrant, note, by }. | Cancel with reason. |
warrant:deny | A judge denies a pending warrant. | { id, warrant, note, by }. | Cancel with reason. |
warrant:serve | A warrant is marked served (tablet or ServeWarrant). Processing a suspect also serves arrest warrants, without this hook. | { id, warrant, note, by }. | Cancel with reason. |
warrant:cancel | A warrant is cancelled. | { id, warrant, note, by }. | Cancel with reason. |
bolo:create | A BOLO is created. | before { type, title, description, identifier, plate, image, priority, department, by }; after { bolo, by }. | Cancel with reason. Modifiable: description, priority. |
bolo:update | A BOLO is edited. | before { id, bolo, type, title, description, identifier, plate, image, priority, by } (bolo is the old version); after { id, bolo, by }. | Cancel with reason. Modifiable: description, priority. |
bolo:close | A BOLO is closed. | { id, bolo, by }. | Cancel with reason. |
citation:create | A citation is issued (tablet, citation book, export). | before { identifier, name, department, violations, total, points, plate, location, notes, coords, by }; after { citation, by }. | Cancel with reason. Modifiable: total, points, notes. |
citation:pay | A citation is paid or marked paid. | { id, citation, manual, by }. | Cancel with reason. |
citation:void | A citation is voided. | { id, citation, reason, by }. | Cancel with reason. |
evidence:create | Evidence is logged. | before { incidentId, label, type, serial, description, location, by }; after { id, evidence, by }. | Cancel with reason (AddEvidence returns nil). Modifiable: label, description, location. |
evidence:update | Evidence is edited. | before { id, evidence, incidentId, label, type, serial, description, location, status, by }; after { id, evidence, by }. | Cancel with reason. Modifiable: label, description, location. |
evidence:transfer | Evidence moves in the chain of custody. | before { id, evidence, action, status, location, to, note, by }; after { id, evidence, action, to, note, by }. | Cancel with reason. |
evidence:delete | Evidence is deleted. | { id, evidence, by }. | Cancel with reason. |
weapon:save | A weapon is registered or edited (tablet, RegisterWeapon, SetWeaponStatus). | { serial, identifier, owner, model, label, status, notes, by }. | Cancel with reason. Modifiable: status, notes, label. |
vehicle:flags | Vehicle flags or points change (tablet, UpdateVehicle, SetVehicleFlag). | { plate, flags, points?, flag?, state?, by }. | Cancel with reason from the tablet, cancel only from SetVehicleFlag. |
citizen:update | A profile is edited, a note or flag added. | { identifier, notes?, tags?, image?, fingerprint?, dna?, note?, tag?, state?, by }. | Cancel with reason from the tablet, cancel only from AddCitizenNote / SetCitizenFlag. |
citizen:license | A license is granted or revoked through the MDT. | before { identifier, type, grant, by }; after adds licenses. | Cancel with reason. |
exports['koja-mdt']:registerHook('incident:process', function(payload)
if payload.months > 60 then return { months = 60 } end
end, { stage = 'before' })
exports['koja-mdt']:registerHook('citation:create', function(payload, src)
if payload.total > 5000 then return false, 'over_limit' end
end, { stage = 'before' })Officers and tablet
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
officer:duty | Duty on or off (tablet toggle, command, SetDuty, framework duty, ambulance sync). | { source, onDuty, department, officer }. | Cancel only, and only for the tablet toggle, the duty command and SetDuty. Framework-driven changes fire the after stage only. |
mdt:open | The tablet is about to open. | { source, officer }. | Cancel only. |
mdt:close | The tablet was closed (reported by the client). | { source, officer }. | After only. |
exports['koja-mdt']:registerHook('mdt:open', function(payload)
return exports.ox_inventory:Search(payload.source, 'count', 'mdt_tablet') > 0
end, { stage = 'before' })Communication
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
mail:send | MDT mail is sent. | { subject, body, recipients, identifiers, by }, after adds id. | Cancel with reason. |
notification:send | Any notification center entry is created (exports, mail, court, citations, roster, calendar, …). | { kind, title, body, actor, link, department, identifiers }. | Cancel only. Modifiable: title, body. |
bulletin:create | A bulletin is posted. | { title, content, department, pinned, by }, after adds id. | Cancel with reason. |
bulletin:update | A bulletin is edited. | before { id, bulletin, title, content, pinned, by }; after { id, bulletin, by }. | Cancel with reason. Modifiable: title, content, pinned. |
bulletin:delete | A bulletin is deleted. | { id, bulletin, by }. | Cancel with reason. |
calendar:create | A calendar event is created. | before { title, description, category, location, startAt, endAt, allDay, department, divisionId, recurrence, by }; after { id, title, category, start, department, by }. | Cancel with reason. Modifiable: title, description, location. |
calendar:cancel | An event is cancelled or restored. | { id, title, restore, reason, by }. | Cancel with reason. |
calendar:delete | An event is deleted. | { id, title, by }. | Cancel with reason. |
Custody
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
jail:create | A jail record is stored (MDT processing, court sentence, ReportJail). | before { identifier, name, months, reason, charges, incidentId, caseId, facility, system }; after { id, record }. | Cancel only: the record is not stored. Modifiable: months, reason, facility. |
jail:release | A sentence ends (MDT early release, ReportRelease, transfer). | { id, status, reason, record, by }; status is released, escaped or transferred. | Only for MDT early releases (tablet, ReleaseInmate). Cancel with reason. |
impound:create | A vehicle is impounded. | before { plate, reason, fee, hours, lot, by }; after { id, plate, record, system }. | Only for MDT impounds (tablet, ImpoundVehicle). Cancel with reason. Modifiable: reason, fee, hours. |
impound:release | An impound is released. | { id, plate, feeStatus, record, by }. | Only for MDT releases (tablet, ReleaseImpound). Cancel with reason. |
Court
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
court:create | A case is filed. | before { title, caseType, incidentId, defendantIdentifier, defendantName, judge, prosecutor, defense, courtroom, sealed, notes, by }; after { id, case, by }. | Cancel with reason. Modifiable: title, courtroom, notes. |
court:status | The case status is changed with the status action (SetCourtCaseStatus). Automatic moves after pleas, hearings and verdicts do not fire it. | before { id, case, status, previous, note, by }; after replaces case with the new version. | Cancel with reason. |
court:verdict | A verdict is entered. | before { id, case, verdict, by }; after { id, verdict, note, case, by }. | Cancel with reason. |
court:sentence | A stored sentence is applied (ApplyCourtSentence). | before { id, case, identifier, target, fine, months, by }; after { id, identifier, target, fine, months, fined, jailed, by }. | Cancel with reason. Modifiable: fine, months. |
EMS
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
treatment:create | An ePCR is created. | before { id, identifier, name, status, type, location, complaint, transport, hospital, priority, outcome, narrative, callId, department, by }; after { id, treatment, by }. | Cancel with reason. Modifiable: location, complaint, narrative. |
treatment:update | An ePCR is saved again. | Same as treatment:create. | Same. |
bill:create | A medical bill is issued. | before { treatmentId, identifier, name, department, items, total, by }; after { id, bill, by }. | Cancel with reason. Modifiable: total. |
bill:status | A bill is marked paid, voided or charged. | { id, status, reason, charged, bill, by }. charged = true when the money was taken from the patient. | Only for mark paid / void. Cancel with reason. |
hospital:admit | A patient is admitted. | before { hospital, bed, identifier, name, treatmentId, state, reason, by }; after { id, admission, by }. | Cancel with reason. |
hospital:discharge | A patient is discharged. | { id, admission, note, by }. | Cancel with reason. |
ANPR
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
anpr:hit | A plate hit is about to be logged (in-car ALPR, CCTV ANPR, ReportAnprRead). Cooldown-suppressed reads do not fire it. | before { plate, source, cameraId, cameraLabel, model, street, coords, reasons, result, officer }; after { id, hit, reasons, dispatchId }. | Cancel only: nothing is logged and no call is created. |
Bodycam recordings
| Hook | Fires when | Payload | Before stage |
|---|---|---|---|
bodycam:recordingSaved | Once per session, about 45 seconds after the recording stops, when the last frames are in. Not fired when no frames were stored. | { sessionId, identifier, frames, bytes, format, firstAt, lastAt, intervalMs }. | After only. |
bodycam:recordingDeleted | Footage is removed. | { sessionId, reason, frames, bytes, by }. reason: manual, session_deleted, retention, disk_cap; by is set for manual deletes. | After only. |
exports['koja-mdt']:registerHook('bodycam:recordingSaved', function(payload)
print(('bodycam session %s saved, %d frames'):format(payload.sessionId, payload.frames))
end)Recipes
exports['koja-mdt']:registerHook('warrant:create', function(payload, src)
if payload.type == 'search' and not payload.reason:find('affidavit') then
return false, 'affidavit_required'
end
end, { stage = 'before' })
exports['koja-mdt']:registerHook('jail:create', function(payload)
if payload.months > 120 then return { months = 120, reason = (payload.reason or '') .. ' (capped)' } end
end, { stage = 'before' })
exports['koja-mdt']:registerHook('hospital:admit', function(payload)
exports['my-hospital']:ReserveBed(payload.admission.hospital, payload.admission.bed)
end)