FiveM police job setup: grades, duty, armory and police count

How to set up a police job on a FiveM server: job and grades, duty toggle, armory with shop and stash, cuffing and escorting, police count checks, MDT and dispatch.

The symptom: you added a police job, but nobody can go on duty, the armory is open to everyone, or robberies say "not enough police" while three officers are online.

A working police job is a handful of small parts that have to agree on one thing: the job name and who is on duty. This article walks through each part and where it usually breaks.

The job and its grades

The job is a row of data: a name, a label and a list of grades (cadet, officer, sergeant, chief). The grade number is what you check later, so decide the order first and keep it. How to create the job in each framework is covered in adding a job on ESX and adding a job on QBCore. On QBox the job lives in the qbx_core shared data instead of a qb-core file.

Two rules that save pain:

  • Use one internal name everywhere, for example police. Every script that says "police only" compares against that string.
  • Give the boss grade the flag your framework uses for bosses, so society and management menus work.

The duty toggle

Being a police officer and being on duty are not the same thing. Robberies, dispatch and paychecks should only count officers on duty.

  • QBCore and QBox keep an onduty field on the job, and a toggle changes it. A job can be set to start on duty or off duty by default.
  • ESX has no duty field in the base job. Many setups use a second off-duty job, or a flag your police resource keeps, so check how yours does it before you write any count.

Whatever the framework, the toggle should run on the server, and the server decides the value. A client event that says "I am on duty" with no check is an exploit.

The armory: shop or stash

Do not hand out weapons from a client event. Use your inventory's own tools. With ox_inventory you can restrict a shop or a stash to a job and a minimum grade, which is the cleanest armory:

lua
-- shop entry (in the ox_inventory shops data)
PoliceArmory = {
    name = 'Police Armory',
    groups = { police = 0 },          -- job name and minimum grade
    inventory = {
        { name = 'radio', price = 0 },
        { name = 'handcuffs', price = 0 },
    },
    locations = { vec3(452.6, -980.0, 30.7) },
}

A stash works the same way for evidence lockers and shared storage. See ox_inventory shops and ox_inventory stashes. Keep the names groups and the grade check the same as your job name, or the armory stays locked for everyone.

Cuffing and escorting

Cuffing is a set of small pieces, not one feature:

  1. The officer plays an animation and the server checks the target is close and the officer has the job.
  2. The target gets a cuffed state, a cuffed animation and disabled controls (no firing, no running, no entering vehicles on their own).
  3. Escorting attaches the target to the officer, or follows them, until released.

Keep the cuffed flag as a state bag so every client sees it, and so players who stream in later see it too. Check distance and job on the server, the way securing your server events describes. Remember to clear the flag when the player is released, dies or leaves.

Police count for robberies

Robberies, heists and some drug jobs ask "how many officers are on duty". The answer must come from the server:

lua
-- ESX (server)
local function policeOnline()
    local count = 0
    for _, xPlayer in pairs(ESX.GetExtendedPlayers('job', 'police')) do
        count += 1
    end
    return count
end
lua
-- QBCore (server)
local function policeOnline()
    local count = 0
    for _, player in pairs(QBCore.Functions.GetQBPlayers()) do
        local job = player.PlayerData.job
        if job.name == 'police' and job.onduty then
            count += 1
        end
    end
    return count
end

On QBox, recent qbx_core versions also ship duty-count exports, so check its documentation before writing your own loop. If you use an off-duty job pair on ESX, count the on-duty name only.

Always count on the server, and count again when the robbery starts, not only when the player walks up. See robbery scripts for the full flow.

MDT and dispatch are separate

The police job gives you a job and a count. These are usually other resources:

  • MDT: records, warrants, citizens and vehicles lookup.
  • Dispatch: 911 calls, alert blips, officer status.
  • Evidence and impound: often their own scripts again.

Check each one's config for the job name. A dispatch that never shows alerts is almost always configured for a different job name than the one you created.

Checklist

Symptom Fix
Armory open to everyone Add the job name and a minimum grade to the shop or stash groups
Armory locked for officers The job name in the armory does not match the job name you created
Robbery says no police, officers online Count only on-duty officers, with the exact job name, on the server
Dispatch shows no alerts Set the police job name in the dispatch config
Player stays cuffed after release Clear the cuffed state on release, death and disconnect
Anyone can go on duty Do the toggle on the server and check the job first

Quick answers

How do I count the police officers online?

On the server, loop over the connected players and count the ones whose job is your police job and who are on duty. Do it on the server, never on the client, because robberies and heists trust that number.

Does a police job include an MDT and dispatch?

Usually not. The job itself is only the job, grades, duty and an armory. The MDT (records, warrants) and the dispatch (calls, blips) are separate resources that read the player's job.

Where do I give police their weapons?

Put them in an armory: an inventory shop or a stash restricted to the police job and to a minimum grade. Do not give weapons from a client event.

Scripts that skip this problem

CCTV Security CamerasPlaceable cameras, a live multi-view tablet and printed evidence photos.View script →Advanced BoostingTablet-driven vehicle boosting: contracts from class D to S+, crews and a live queue.View script →

Keep reading