Jobs framework: clock-in, grades, paychecks
A job is a small state machine: off-duty or on-duty, plus a job name and grade that decide what the player may do. This lesson builds a framework-reading demo that keeps its own temporary duty table, gates one action by both job and grade, and calculates payroll on a timer. It prints the calculated pay; it does not change framework money or framework duty state.
Build it
Make the resource folder
Inside your server's resources folder, create this folder:
resources/qu_jobs_framework
Create the files
Create this exact file layout:
resources/qu_jobs_framework/
fxmanifest.lua
server.lua
Write fxmanifest.lua
Open fxmanifest.lua and paste this:
fx_version 'cerulean'
game 'gta5'
server_script 'server.lua'
No lua54 'yes' line. As of June 2025 that directive is deprecated and ignored: Lua 5.4 is now the only Lua runtime, so you leave it out.
Write the lesson code
Open server.lua and paste this:
-- qu_jobs_framework: a framework-agnostic job spine.
-- Reads job + grade from whichever framework is running, toggles duty,
-- gates one grade-locked action, and pays players on a clock.
local duty = {} -- duty[src] = true|false, keyed by server id
local PAY_INTERVAL_MS = 60000 -- pay every 60 seconds while on duty
local BOSS_JOB = 'police' -- the job allowed to use /jobaudit
local BOSS_GRADE = 2 -- minimum grade.level allowed to run /jobaudit
-- Return a normalised { name, grade, label } for the player on src,
-- regardless of which framework is installed. nil if no player is found.
local function getJob(src)
if GetResourceState('qbx_core') == 'started' then
local player = exports.qbx_core:GetPlayer(src)
if not player then return nil end
local job = player.PlayerData.job
return { name = job.name, grade = job.grade.level, label = job.grade.name }
elseif GetResourceState('qb-core') == 'started' then
local QBCore = exports['qb-core']:GetCoreObject()
local player = QBCore.Functions.GetPlayer(src)
if not player then return nil end
local job = player.PlayerData.job
return { name = job.name, grade = job.grade.level, label = job.grade.name }
elseif GetResourceState('es_extended') == 'started' then
local ESX = exports.es_extended:getSharedObject()
local player = ESX.GetPlayerFromId(src)
if not player then return nil end
local job = player.getJob()
return { name = job.name, grade = job.grade, label = job.grade_label }
end
return nil
end
RegisterCommand('clockin', function(src)
local job = getJob(src)
if not job then
print(('[qu_jobs_framework] %s has no framework player (are you a loaded character?)'):format(GetPlayerName(src)))
return
end
duty[src] = not duty[src]
print(('[qu_jobs_framework] %s job %s grade %d (%s) duty %s'):format(
GetPlayerName(src), job.name, job.grade, job.label, tostring(duty[src])
))
end, false)
-- Grade-gated action: only a player at BOSS_GRADE or higher may run it.
RegisterCommand('jobaudit', function(src)
local job = getJob(src)
if not job then return end
if job.name ~= BOSS_JOB or job.grade < BOSS_GRADE then
print(('[qu_jobs_framework] %s denied /jobaudit (%s grade %d)'):format(GetPlayerName(src), job.name, job.grade))
return
end
print(('[qu_jobs_framework] %s ran /jobaudit as %s grade %d'):format(GetPlayerName(src), job.name, job.grade))
end, false)
-- Payroll loop: every interval, pay everyone currently on duty.
CreateThread(function()
while true do
Wait(PAY_INTERVAL_MS)
for src, onDuty in pairs(duty) do
if onDuty then
local job = getJob(src)
if job then
local pay = 50 + (job.grade * 25) -- base + grade bonus
print(('[qu_jobs_framework] payroll: %s (%s grade %d) earned $%d'):format(
GetPlayerName(src), job.name, job.grade, pay
))
end
end
end
end
end)
-- Clear duty state when a player leaves, or the table leaks server ids forever.
AddEventHandler('playerDropped', function()
duty[source] = nil
end)
Start and test it
Open server.cfg and add this line below your framework's ensure line so the framework loads first:
ensure qu_jobs_framework
Save, then run:
restart qu_jobs_framework
Join the server with a loaded character, then run this from the F8 client console (these commands are server commands; the player's input is relayed to the server):
clockin
Read the result in txAdmin Live Console. On a typical default setup an unemployed character produces a line like this; your starting job and grade label can differ:
Leave the duty toggle on and wait one minute. The payroll loop fires and prints:
Keep reading the full lesson
Sign in to start, then unlock every step of this lesson and the full FiveM School with a membership.
- How it works
- If something went wrong
- What you can do now
- Try it yourself
The remainder of Jobs framework is available to FiveM School members.