lation scripts
scriptsmembershipdocs
see the scripts
Skip to Content
lation ui
lation interact
lation banking
lation shops
lation jobs
Admin panelBoss menuPlayersConfigurationDevelopersChangelog
lation weed
lation chop shop

classics

money laundering
drug sales
coke processing
meth crafting
metal detecting
scuba diving
lation jobs

Developers

The lation jobs server and client exports, the events it fires, its server hooks, Script places and custom place types, with signatures and Lua examples.

A job is addressed by its name: the job or gang name the framework knows it by. An employee is an identifier (the citizenid on Qbox and QBCore, the player identifier on ESX), online or not. A grade is a level: 0 is the lowest grade and the boss grade is the highest. Every export, event, payload key and hook parameter calls a job job.

The writes go through the same paths the panels use, so the hooks, the activity log, the bank membership, the logs and the live refresh of every open panel all hold whoever calls.

Server exports

Reads

ExportReturns
GetJob(name)A job, or nil
GetJobs()Every managed job, in label order
GetGrades(name)The job's grades, lowest first
GetEmployees(name)The roster
GetMemberships(identifier)Every job and gang a player holds
IsEmployee(name, identifier)boolean
GetEmployeeGrade(name, identifier)A grade, or nil
IsOnDuty(target)boolean, and the job they are on duty at
GetOnDuty(name)Who is on duty at a job
GetPlaces(name, type?)The job's places, optionally of one kind
GetConfig(section?)A copy of the live configuration

Reads for a job that is not managed here return nil, false or an empty table.

GetJob

Server export

exports.lation_jobs:GetJob(name)

Parameters

namestring
The job or gang name

Returns

table | nil
The job (below), or nil

The job:

{
    name = 'police',
    kind = 'job',         -- 'job' or 'gang'
    label = 'LSPD',
    icon = nil,           -- string or nil
    color = nil,          -- '#RRGGBB' or nil
    blip = nil,           -- { enabled, sprite, color, scale, label } or nil
    settings = {
        hiring = true,        -- applications open
        public = true,        -- listed on the job board
        description = '',     -- the job board sentence
        payroll = {},         -- { intervalMinutes?, shortfall?, source? }, this job's overrides
        salaryMax = nil,      -- this job's own salary cap
        hiringMode = nil,     -- 'direct', 'offer' or nil for the server default
        application = { questions = {} },
        webhook = { set = false, serverToo = false }, -- the URL never leaves the server
    },
    grades = {},          -- grades, lowest first (below)
    places = {},          -- places (below)
    createdBy = 'ABC12345',
    createdAt = 1730000000,
}

A grade:

{
    id = 12,
    level = 0,
    name = 'officer',          -- 'boss' for the boss grade
    label = 'Officer',
    salary = 120,              -- per hour on duty
    permissions = { hire = false, fire = false, --[[ every permission key ]] },
    isboss = false,
    outfit = nil,
}

A place:

{
    id = 7,
    type = 'stash',
    label = 'Evidence',
    coords = { x = 0.0, y = 0.0, z = 0.0, w = 0.0 },
    options = { slots = 50, weight = 100, personal = false }, -- the kind's own fields
    minGrade = 0,
    access = 'grade',          -- 'grade' (minGrade and up) or 'grades' (only the levels in grades)
    grades = nil,
    onDutyOnly = true,
    icon = nil,
    pin = false,
    createdBy = 'ABC12345',
    createdAt = 1730000000,
}

Times are unix seconds.

GetEmployees

Server export

exports.lation_jobs:GetEmployees(name)

Parameters

namestring
The job or gang name

Returns

table
The roster, highest grade first (below)

The roster:

{
    {
        identifier = 'ABC12345',
        name = 'Jane Doe',
        avatar = nil,         -- picture URL or nil
        grade = 3,
        gradeLabel = 'Chief',
        isboss = true,
        online = true,
        onDuty = false,
        onLeave = nil,        -- the last day of approved leave covering today, or nil
        hiredAt = 1730000000,
        hiredBy = 'XYZ98765',
        lastSeen = 1730500000,
    },
}
server.lua
for _, employee in ipairs(exports.lation_jobs:GetEmployees('police')) do
    if employee.onDuty then print(employee.name, employee.gradeLabel) end
end

GetMemberships

Server export

exports.lation_jobs:GetMemberships(identifier)

Parameters

identifierstring
The player's identifier

Returns

table
{ name, label, kind, grade } for every job and gang the player holds, in label order

Only one of them is the framework's active job; read the framework for that.

IsEmployee and GetEmployeeGrade

Server export

exports.lation_jobs:IsEmployee(name, identifier)       -- boolean
exports.lation_jobs:GetEmployeeGrade(name, identifier) -- grade or nil

Parameters

namestring
The job or gang name
identifierstring
The player's identifier

Returns

IsEmployee returns a boolean; GetEmployeeGrade returns a grade, or nil

IsOnDuty

Server export

local onDuty, job = exports.lation_jobs:IsOnDuty(target)

Parameters

targetnumber | string
A server id, or the identifier of a connected player

Returns

boolean, string
Whether the player is on duty and the name of the job they are clocked on at. A player who is not connected returns false.
server.lua
local onDuty, job = exports.lation_jobs:IsOnDuty(source)
if onDuty and job == 'ambulance' then
    -- an EMS employee on shift
end

GetOnDuty

Server export

exports.lation_jobs:GetOnDuty(name)

Parameters

namestring
The job or gang name

Returns

table
{ source, identifier, name, grade, startedAt } for everyone clocked on at the job, longest shift first

GetPlaces

Server export

exports.lation_jobs:GetPlaces(name, type)

Parameters

namestring
The job or gang name
typestringoptionaldefault nil
One kind ('stash', 'garage', ...); all kinds when left out

Returns

table
The job's places, optionally of one kind

GetConfig

Server export

exports.lation_jobs:GetConfig(section)

Parameters

sectionstringoptionaldefault nil
general, businesses, duty, work, hiring, payroll, applications, forms, notices, leave, jobcenter, places or activity

Returns

table
A copy of that section, nested by the keys on the configuration page, or every section when left out. The logs section is never returned.
server.lua
local work = exports.lation_jobs:GetConfig('work')
print(work.maxJobs) -- 3

Writes

The writes return true, or false and an error code; CreateJob returns the job, or nil and a code. The codes are the keys under errors in locales/en.json (not_employed, grade_denied, already_employed ...), where the text the panels show for each one lives. A job that is not managed here is job_unknown from Hire and DeleteJob, and invalid from the others. When a hook refuses, its own message is the code.

CreateJob

Server export

local job, err = exports.lation_jobs:CreateJob(data)

Parameters

data.namestring
The framework name: lowercase letters, digits and underscores, starting with a letter, up to 40 characters
data.labelstring
What players see
data.kindstringoptionaldefault 'job'
'job' or 'gang'

Returns

table | nil, string
The new job, or nil and an error (invalid_name, job_exists, label_required, gangs_disabled, gangs_unsupported, too_many_grades, database)

A name the framework already has is adopted, as from the panel; a new one starts with a boss grade and an employee grade. Fires lation_jobs:jobCreated.

server.lua
local job, err = exports.lation_jobs:CreateJob({ name = 'taxi', label = 'Downtown Cab Co.' })
if not job then print(('could not create the job: %s'):format(err)) end

DeleteJob

Server export

local ok, err = exports.lation_jobs:DeleteJob(name)

Parameters

namestring
The job or gang name

Returns

boolean, string
true, or false and an error code: job_unknown for a job that is not managed here, database when the delete could not be written

Deletes the job as the Danger zone does: employees to unemployed, shifts closed, the framework forgets the job, the bank keeps the account. Fires lation_jobs:jobDeleted.

server.lua
local ok, err = exports.lation_jobs:DeleteJob('taxi')
if not ok then print(('could not delete the job: %s'):format(err)) end

Hire

Server export

local ok, err = exports.lation_jobs:Hire(name, target, grade, actor)

Parameters

namestring
The job or gang name
targetnumber | string
A connected player's server id, or an identifier (online or not)
gradenumber
The grade level to hire at
actornumberoptionaldefault nil
The server id of whoever is hiring, for the activity log, the hooks and the hired event

Returns

boolean, string
true, or false and an error code

Seats the person at once, even when the job hires by offer, with no grade ceiling. canHire still runs, and the cap on jobs per player still holds (too_many_jobs). An offline hire takes effect on their next load.

server.lua
local ok, err = exports.lation_jobs:Hire('police', source, 0)
if not ok then print(('hire refused: %s'):format(err)) end

Fire

Server export

local ok, err = exports.lation_jobs:Fire(name, identifier, actor)

Parameters

namestring
The job or gang name
identifierstring
Who to fire
actornumberoptionaldefault nil
The server id of whoever is firing

Returns

boolean, string
true, or false and an error code

Closes their shift there, removes their access to the society account at lation banking, and hands the framework their next job or unemployed. canFire runs first.

SetGrade

Server export

local ok, err = exports.lation_jobs:SetGrade(name, identifier, grade, actor)

Parameters

namestring
The job or gang name
identifierstring
Whose grade to change
gradenumber
The new grade level
actornumberoptionaldefault nil
The server id of whoever is changing it

Returns

boolean, string
true, or false and an error code

Fires lation_jobs:promoted or lation_jobs:demoted.

SetDuty

Server export

local ok, err = exports.lation_jobs:SetDuty(source, onDuty)

Parameters

sourcenumber
The player's server id
onDutyboolean
true to clock on, false to clock off

Returns

boolean, string
true, or false and an error code

Clocking on picks the job the way /duty does: the active job, then the gang, then the one job the player holds (pick_job when they hold several and none is active). canClockOn runs first. A player already in the state asked for returns true.

SwitchJob

Server export

local ok, err = exports.lation_jobs:SwitchJob(source, name)

Parameters

sourcenumber
The player's server id
namestring
One of the jobs they hold

Returns

boolean, string
true, or false and an error code

Makes it the player's active job: what the framework and every other script see. Jobs only, and refused with on_duty_elsewhere while they are clocked on at another job.

RegisterPlaceType

See custom place types.

Client exports

ExportReturnsDescription
openBoss(job?)Opens the boss menu on that job, or on the first job the player runs when none is given
openAdmin()Opens the admin panel; the server checks the permission
openJobs()Opens the Work panel
isOnDuty(job?)booleanWhether the player is on duty, at that job when one is given
getDutyJob()string or nilThe job the player is clocked on at
close()Closes whichever panel is open
isOpen()booleanWhether a panel is open
registerPlaceType(type, def)booleanThe client half of a custom place type
askPlace(place, action, data?)tableSends a prompt of a custom place type to the server
client.lua
if exports.lation_jobs:isOnDuty('mechanic') then
    exports.lation_jobs:openBoss('mechanic')
end

The duty statebag

While a player is on duty, the replicated player statebag lation_duty holds the job name; off duty it is false. Read it as Player(source).state.lation_duty on the server or LocalPlayer.state.lation_duty on the client.

The garage statebag

A vehicle a garage place spawns from its list (not a jg garage's) carries the replicated entity statebag lation_jobs. Read it as Entity(vehicle).state.lation_jobs on the server or the client:

{
    job = 'police',       -- the job name
    owner = 'ABC12345',   -- the identifier of the employee it was handed to
    place = 9,            -- the id of the garage place it came from
}

Events

Every change fires a server event once it is done. They are local server events, so listen with AddEventHandler.

EventArguments
lation_jobs:readyNone. Every job is loaded and registered with the framework.
lation_jobs:jobCreatedname
lation_jobs:jobUpdatedname
lation_jobs:jobDeletedname
lation_jobs:hiredjob, identifier, grade, actor
lation_jobs:firedjob, identifier, actor
lation_jobs:quitjob, identifier
lation_jobs:promotedjob, identifier, grade, actor
lation_jobs:demotedjob, identifier, grade, actor
lation_jobs:dutyChanged{ source, identifier, job, onDuty, reason? }
lation_jobs:payroll{ job, rows, shortfall, total }
lation_jobs:bonus{ job, identifier, amount, note, actor }
lation_jobs:deposit{ job, identifier, amount }
lation_jobs:withdraw{ job, identifier, amount }
lation_jobs:placeAddedjob, placeId, type
lation_jobs:placeRemovedjob, placeId, type
lation_jobs:placeUsedsource, job, placeId, type, action
lation_jobs:applicationCreated{ job, identifier, id }
lation_jobs:applicationAnswered{ job, identifier, id, status, grade?, actor }
lation_jobs:formSubmitted{ job, identifier, form, response }
lation_jobs:noticePosted{ job, id, author }
lation_jobs:offerSent{ job, id, identifier, grade, actor, application }
lation_jobs:offerAnswered{ job, id, identifier, status, grade, actor? }
lation_jobs:leaveRequested{ job, id, identifier, from, to }
lation_jobs:leaveAnswered{ job, id, identifier, status, actor }
lation_jobs:configUpdatedsection or nil

Notes on the arguments:

  • actor is the server id of the player who made the change, or nil (another resource without an actor, a job another script handed out through the framework, an accepted offer).
  • dutyChanged.reason is set when a shift closes: command, place, panel, framework, boss, disconnect, idle, dead, fired or deleted.
  • payroll.rows has one entry per employee in the run: { identifier, name, grade, gradeLabel, minutes, amount, paid, reason? }.
  • placeUsed.action is what the prompt asked: toggle (duty), open (stash, jg garage), list, spawn and return (locker and garage), store (jg garage), enter and exit (teleport), use (script), or a custom type's own.
  • applicationAnswered.status is accepted, declined or offered; offerAnswered.status is accepted, declined or withdrawn.
  • deposit and withdraw name the boss who moved the money.
server.lua
AddEventHandler('lation_jobs:hired', function(job, identifier, grade, actor)
    if job == 'police' then
        print(('%s joined the police at grade %d'):format(identifier, grade))
    end
end)

AddEventHandler('lation_jobs:dutyChanged', function(data)
    if data.job == 'ambulance' and not data.onDuty then
        print(('%s clocked off (%s)'):format(data.identifier, data.reason or 'manual'))
    end
end)

Client events

EventArgumentsFires when
lation_jobs:dutyChangedjob or nilThe player clocks on or off
lation_jobs:membershipsChangedNoneThe jobs the player holds, or their grades, change
lation_jobs:configUpdatedNoneThe live configuration the client holds changed

Script places

The quickest way to hook your own script needs no code on the lation jobs side. An admin puts down a Script place and names what it fires. lation jobs checks the grade, the duty and the distance as for every place, then fires it with the player and the job attached, so your script runs only for the people you chose, where you put it. Only an admin sets one, and the place data sent to clients leaves out what it fires.

FiresNameCalled as
A client eventmymechanic:repairTriggerEvent(name, payload) on the player's client
A client exportmyradio.openexports[resource][function](payload) on the player's client
A server eventmycrafting:openTriggerEvent(name, source, payload) on the server
A server exportmyturf.claimexports[resource][function](source, payload) on the server
A commandme checks the boardExecuteCommand(name) on the player's client, as if typed

An export's name is resource.function, and the resource must exist on the server when the place is saved; a server export's resource must also be running when the place is used. A command is the line without the slash, arguments included, and gets no payload. Names run up to 80 characters.

The place also takes a Prompt (what the row says, up to 40 characters, Use when empty), a Hold time in milliseconds, and Arguments: a JSON object or list, up to 2,000 characters, handed to your script as args.

A Script place's dialog: it fires a client event named mymechanic:repair, with no prompt, a hold of 1000 ms, and bench set to engine as its arguments
What a Script place fires

The payload:

{
    job = 'mechanic',                     -- the job name
    label = 'Bennys',                     -- the job label
    place = { id = 14, label = 'Lift' },
    grade = 2,                            -- the player's grade level
    gradeLabel = 'Mechanic',
    onDuty = true,                        -- on duty at this job
    args = { bench = 'engine' },          -- the JSON typed into the place, or nil
}
client.lua
-- Fires: a client event, name mymechanic:repair
AddEventHandler('mymechanic:repair', function(payload)
    StartRepair(payload.job, payload.grade)
end)

-- Fires: a client export, name myradio.open (in the resource myradio)
exports('open', function(payload)
    OpenRadio(payload.job)
end)
server.lua
-- Fires: a server event, name mycrafting:open
AddEventHandler('mycrafting:open', function(source, payload)
    OpenBench(source, payload.args and payload.args.bench)
end)

-- Fires: a server export, name myturf.claim (in the resource myturf)
exports('claim', function(source, payload)
    ClaimTurf(source, payload.job)
end)

The server event is fired by the server itself, so a plain AddEventHandler is enough and clients cannot trigger it. Every use also fires lation_jobs:placeUsed.

Custom place types

A kind of place of your own is two halves: the server half cleans what the dialog sends and answers a prompt once the grade, duty and distance checks have passed; the client half builds the prompts. Admins and bosses put it down from the same dialog as the built-in kinds. The type belongs to the resource that registered it: when that resource stops, the type goes and its places stay dark until it is back. When lation_jobs restarts, register again (it fires lation_jobs:ready on the server once it has loaded).

RegisterPlaceType (server)

Server export

exports.lation_jobs:RegisterPlaceType(type, def) -- boolean

Parameters

typestring
Lowercase letters, digits and underscores, starting with a letter. A kind another resource owns is refused, built-in ones included.
def.labelstringoptionaldefault nil
The name the panel shows for the kind
def.iconstringoptionaldefault 'map-pin'
The kind's icon
def.fieldstableoptionaldefault {}
What the dialog asks for (below)
def.coercefunctionoptional
coerce(options, job, existing) returns the cleaned options, or nil and a message to refuse the save. existing is the place being edited, nil for a new one. Left out, the options pass through as they are.
def.onSelectfunctionoptionaldefault nil
onSelect(source, place, job, action, data, grade); without it the kind has nothing to answer
def.onCreatefunctionoptionaldefault nil
onCreate(place, job), after a place of the kind is saved
def.onRemovefunctionoptionaldefault nil
onRemove(place, job), when one is removed. An edit runs onRemove on the old place, then onCreate on the new one.

Returns

boolean

The callbacks get the shapes the reads return: job is the job as GetJob returns it, place is a place as it shows there plus place.job, the name of the job it belongs to, and grade is the player's grade as GetGrades returns it (level, label, isboss ...). onSelect returns true or a table (sent back to the client with ok = true), or false and a message to refuse.

A refusal from coerce (to whoever saves the place) or onSelect (to the player at it) is shown as written ("The lift is already up"), or, when it is a key under errors in locales/*.json (lift_up), in the server's language.

Each field is a table:

KeyDescription
keyWhere the value lands in options. The dialog labels a custom kind's field with its key.
typetext, number, boolean, select, coords, vehicles, attach or json
min, max, step, unitFor numbers
maxFor text, the most characters
optionsFor a select, the values to pick from
optionalThe field may be left empty
when{ key, value }: show the field only while another field holds that value

registerPlaceType and askPlace (client)

Client export

exports.lation_jobs:registerPlaceType(type, def) -- boolean
exports.lation_jobs:askPlace(place, action, data) -- { ok, message?, ... }

Parameters

def.optionsfunctionoptionaldefault nil
options(place, view) returns the prompts: { label, icon, onSelect, canInteract?, hold? } each
def.setupfunctionoptionaldefault nil
setup(place, view, record) registers what is not a prompt
actionstring
Passed to the server's onSelect
datatableoptionaldefault nil
Passed to the server's onSelect

view is the job as the client knows it (name, label, kind, grades, places). Every prompt is gated for you on the place's grades and, when the place is on duty only, on the player's duty. A prompt's onSelect that returns false and a message shows the message inline on lation interact, and as a notification on the other systems.

Example: a car lift

server.lua
exports.lation_jobs:RegisterPlaceType('lift', {
    label = 'Car lift',
    icon = 'wrench',
    fields = {
        { key = 'height', type = 'number', min = 1, max = 5, unit = 'm' }
    },
    coerce = function(options, job)
        local height = tonumber(options.height)
        if not height then return nil, 'invalid' end
        return { height = math.floor(height) }
    end,
    onSelect = function(source, place, job, action, data, grade)
        if action ~= 'raise' then return false, 'invalid' end
        TriggerClientEvent('mylift:raise', source, place.id, place.options.height)
        return true
    end
})
client.lua
exports.lation_jobs:registerPlaceType('lift', {
    options = function(place, view)
        return {
            {
                label = 'Raise the lift',
                icon = 'arrow-up',
                onSelect = function()
                    local result = exports.lation_jobs:askPlace(place, 'raise')
                    if not result.ok then return false, result.message end
                    return true
                end
            }
        }
    end
})

Hooks

server/utils/hooks.lua ships open. Edit the functions in place to plug in whitelists, Discord roles, pay rules or outfits.

HookCalledReturn
Hooks.canHire(actor, job, identifier, grade)Before a hire, from the panels or an exportfalse and an optional message to refuse
Hooks.canFire(actor, job, identifier)Before a firingfalse and an optional message to refuse
Hooks.canClockOn(source, job)Before a player clocks onfalse and an optional message to keep them off duty
Hooks.payrollAmount(job, identifier, grade, minutes, amount)When payroll has worked out what an employee is owedThe amount to pay, in whole units
Hooks.onActivity(row)After every activity row is writtenNothing
Hooks.outfitFor(job, grade)When a locker opens, per grade it offersAn outfit to use instead of the grade's, or nil
Hooks.vehicleSpawned(job, identifier, vehicle)After a garage spawns a vehicle, with the server entityNothing
  • actor is the server id of the player making the change, or nil. A job another script hands out through the framework is taken in without canHire.
  • A message from canHire, canFire or canClockOn is shown as written ("You need your badge to clock on"). A key under errors in locales/*.json (no_badge) is shown in the server's language instead: return the key and add its text there. An export caller gets the message as the error code.
  • payrollAmount returning 0 or less pays nothing this run and carries the minutes to the next, where the hook is asked again with the new minutes added. A grade with no salary earns nothing: when the hook leaves its amount at 0, its minutes are settled.
  • onActivity gets { job, event, actorIdentifier?, actorName?, targetIdentifier?, targetName?, data?, createdAt }. job is '' for a server-wide row, whose event is config (a config edit), job_created, job_imported or job_deleted.
  • outfitFor returns the shape your appearance script applies: { components, props } for illenium-appearance and fivem-appearance.
server/utils/hooks.lua
-- Officers need a badge to clock on (ox_inventory)
function Hooks.canClockOn(source, job)
    if job == 'police' and exports.ox_inventory:Search(source, 'count', 'police_badge') < 1 then
        return false, 'no_badge'
    end
    return true
end

-- Keep 10% back as tax
function Hooks.payrollAmount(job, identifier, grade, minutes, amount)
    return math.floor(amount * 0.9)
end

-- Fill the tank of every garage vehicle (ox_fuel reads this statebag)
function Hooks.vehicleSpawned(job, identifier, vehicle)
    Entity(vehicle).state.fuel = 100
end
locales/en.json
{
  "errors": {
    "no_badge": "You need your badge to clock on"
  }
}

Add the key to the existing errors object rather than replacing the file.

Other open code

  • server/utils/perms.lua: IsAdmin(source) decides who opens /jobadmin.
  • server/bank/providers.lua: one entry per bank (account, ensure, balance, credit, debit, and transactions where the bank has a history to read). A bank of your own is one more entry.
  • bridge/server.lua and bridge/client.lua: the framework, inventory, appearance and keys calls, each with a slot for a custom one.
  • client/utils/interact.lua and client/utils/notify.lua: the interaction and notification systems, each with a Custom slot that Config > General can select.

Still stuck? Ask in our Discord: the people who write the scripts answer.

Last updated November 10, 2026

Configuration

Every live option in lation jobs with its default, edited in game under /jobadmin > Config, since the resource has no config file.

Changelog

Every lation jobs release, newest first, with what it added, changed and fixed.

On this page

Server exportsReadsGetJobGetEmployeesGetMembershipsIsEmployee and GetEmployeeGradeIsOnDutyGetOnDutyGetPlacesGetConfigWritesCreateJobDeleteJobHireFireSetGradeSetDutySwitchJobRegisterPlaceTypeClient exportsThe duty statebagThe garage statebagEventsClient eventsScript placesCustom place typesRegisterPlaceType (server)registerPlaceType and askPlace (client)Example: a car liftHooksOther open code
lation scripts

FiveM scripts for Qbox, QBCore and ESX servers, made to look and work like one product.

store

  • all scripts
  • membership
  • classics

help

  • docs
  • discord
  • email support

legal

  • terms
  • refunds
  • privacy
© 2026 Lation Labslationscripts.com
  • home
  • scripts
  • membership
  • docs