Koja Scripts

Hooks

Every koja-mdt hook, when it fires, its payload, which fields a before hook can change and how cancelling works.

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.

server.lua
local hookId = exports['koja-mdt']:registerHook(name, function(payload, src)
end, { stage = 'before', priority = 0 })
OptionDefaultNotes
stage'after''before' runs before the action is saved and can cancel or change it. 'after' runs once it is done.
priority0Lower 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

  1. Before hooks

    • Return false to 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 payload in 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.
  2. 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.
  3. Arguments

    src is the acting player's server id, or nil for web portal, export and system actions. Most payloads carry by (or officer / unit): { source, identifier, name, callsign, department, system }. system = true marks 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.

server.lua
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

HookFires whenPayloadBefore stage
dispatch:createA 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:updateUpdateDispatch 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:noteA note is added (tablet or AddDispatchNote).before { callId, text, author, by }; after adds call.Cancel with reason. Modifiable: text.
dispatch:assignA unit attaches or is assigned to a call.{ callId, source, unit }, after adds call.Cancel only.
dispatch:detachA unit leaves a call.{ callId, source, unit }.After only.
dispatch:closeA call is closed.{ callId, call, by }.Cancel only.
dispatch:panicAn officer presses panic.before { source, officer, street }; after adds callId, call.Cancel only.
unit:statusA unit changes status.{ source, status, previous, unit }.Cancel with reason.
server.lua
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

HookFires whenPayloadBefore stage
incident:createA 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:updateA report is saved again (tablet, UpdateIncident, AddPersonToIncident, …). AddChargeToIncident fires the after stage only.Same as incident:create, plus id.Same as incident:create.
incident:deleteA report is deleted.{ id, incident, by }.Cancel with reason.
incident:processA suspect is processed (fine / jail).{ incidentId, personId, identifier, name, target, fine, months, applyFine, applyJail, by }.Cancel with reason. Modifiable: fine, months, applyFine, applyJail.
warrant:createA 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:approveA judge approves a pending warrant.{ id, warrant, note, by }.Cancel with reason.
warrant:denyA judge denies a pending warrant.{ id, warrant, note, by }.Cancel with reason.
warrant:serveA 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:cancelA warrant is cancelled.{ id, warrant, note, by }.Cancel with reason.
bolo:createA BOLO is created.before { type, title, description, identifier, plate, image, priority, department, by }; after { bolo, by }.Cancel with reason. Modifiable: description, priority.
bolo:updateA 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:closeA BOLO is closed.{ id, bolo, by }.Cancel with reason.
citation:createA 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:payA citation is paid or marked paid.{ id, citation, manual, by }.Cancel with reason.
citation:voidA citation is voided.{ id, citation, reason, by }.Cancel with reason.
evidence:createEvidence 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:updateEvidence 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:transferEvidence 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:deleteEvidence is deleted.{ id, evidence, by }.Cancel with reason.
weapon:saveA weapon is registered or edited (tablet, RegisterWeapon, SetWeaponStatus).{ serial, identifier, owner, model, label, status, notes, by }.Cancel with reason. Modifiable: status, notes, label.
vehicle:flagsVehicle flags or points change (tablet, UpdateVehicle, SetVehicleFlag).{ plate, flags, points?, flag?, state?, by }.Cancel with reason from the tablet, cancel only from SetVehicleFlag.
citizen:updateA 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:licenseA license is granted or revoked through the MDT.before { identifier, type, grant, by }; after adds licenses.Cancel with reason.
server.lua
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

HookFires whenPayloadBefore stage
officer:dutyDuty 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:openThe tablet is about to open.{ source, officer }.Cancel only.
mdt:closeThe tablet was closed (reported by the client).{ source, officer }.After only.
server.lua
exports['koja-mdt']:registerHook('mdt:open', function(payload)
    return exports.ox_inventory:Search(payload.source, 'count', 'mdt_tablet') > 0
end, { stage = 'before' })

Communication

HookFires whenPayloadBefore stage
mail:sendMDT mail is sent.{ subject, body, recipients, identifiers, by }, after adds id.Cancel with reason.
notification:sendAny notification center entry is created (exports, mail, court, citations, roster, calendar, …).{ kind, title, body, actor, link, department, identifiers }.Cancel only. Modifiable: title, body.
bulletin:createA bulletin is posted.{ title, content, department, pinned, by }, after adds id.Cancel with reason.
bulletin:updateA bulletin is edited.before { id, bulletin, title, content, pinned, by }; after { id, bulletin, by }.Cancel with reason. Modifiable: title, content, pinned.
bulletin:deleteA bulletin is deleted.{ id, bulletin, by }.Cancel with reason.
calendar:createA 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:cancelAn event is cancelled or restored.{ id, title, restore, reason, by }.Cancel with reason.
calendar:deleteAn event is deleted.{ id, title, by }.Cancel with reason.

Custody

HookFires whenPayloadBefore stage
jail:createA 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:releaseA 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:createA 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:releaseAn impound is released.{ id, plate, feeStatus, record, by }.Only for MDT releases (tablet, ReleaseImpound). Cancel with reason.

Court

HookFires whenPayloadBefore stage
court:createA 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:statusThe 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:verdictA verdict is entered.before { id, case, verdict, by }; after { id, verdict, note, case, by }.Cancel with reason.
court:sentenceA 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

HookFires whenPayloadBefore stage
treatment:createAn 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:updateAn ePCR is saved again.Same as treatment:create.Same.
bill:createA medical bill is issued.before { treatmentId, identifier, name, department, items, total, by }; after { id, bill, by }.Cancel with reason. Modifiable: total.
bill:statusA 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:admitA patient is admitted.before { hospital, bed, identifier, name, treatmentId, state, reason, by }; after { id, admission, by }.Cancel with reason.
hospital:dischargeA patient is discharged.{ id, admission, note, by }.Cancel with reason.

ANPR

HookFires whenPayloadBefore stage
anpr:hitA 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

HookFires whenPayloadBefore stage
bodycam:recordingSavedOnce 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:recordingDeletedFootage is removed.{ sessionId, reason, frames, bytes, by }. reason: manual, session_deleted, retention, disk_cap; by is set for manual deletes.After only.
server.lua
exports['koja-mdt']:registerHook('bodycam:recordingSaved', function(payload)
    print(('bodycam session %s saved, %d frames'):format(payload.sessionId, payload.frames))
end)

Recipes

server.lua
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)