RedM documentation

Murphy's Job Board

Advanced

Create a Job - Beginner Guide


How to create your first job from scratch, step by step.

Table of Contents


How It Works

Murphy JobPanel uses a poster board system. Players walk up to a board in the world, interact with it, and see a selection of job posters. When they accept a job, the system guides them through a series of steps (go here, do this, deliver that) until completion, then pays them a reward.

Each job is a single Lua file in config/jobs/. The resource reads all files in that folder automatically - no need to register them anywhere else.

Prerequisites

  • A working RedM server with one of the supported frameworks:
    • VORP Core
    • RSG-Core
    • REDEMRP 2023
    • REDEMRP 2022
    • QB-Core (RedM)
  • murphy_interact resource installed and running
  • mysql-async or oxmysql for database
  • The SQL table created (run murphy_jobpanel.sql)

Your First Job in 5 Minutes

Let's create a simple task job where the player goes to a location and performs an animation.

Step 1: Create the file

Create a new file: config/jobs/my_first_job.lua

Step 2: Paste the template

lua
local JobData = {
name = filename(),
label = "Street Sweeper",
description = "Clean the streets of Armadillo",
steps = {
{ action = "PlayAnimation" },
{ action = "PlayAnimation" },
{ action = "CompleteJob", reward = 30, label = "Complete", description = "Collect your pay" },
},
board = { "armadillo" },
image = "./img/Poster_art/train2.png",
reward = "$30",
}
if IsDuplicityVersion() then
JobsConfig[filename()] = JobData
else
local lang = {
["NextStep"] = "Next step",
["Finish"] = "Job complete!",
}
local orderedLocations = {
{
name = "Sweep the porch",
description = "Sweep in front of the general store",
coords = vector3(-3626.562, -2605.512, -14.346),
anim = "WORLD_HUMAN_BROOM",
duration = 8000
},
{
name = "Clean the window",
description = "Wipe the bank window clean",
coords = vector3(-3670.741, -2622.23, -14.752),
anim = {
dict = "amb_work@world_human_clean_window@male_a@wip_base",
anim = "wip_base"
},
duration = 8000
},
}
local blips = {}
local Utils = {}
local actions = {}
actions["PlayAnimation"] = function(job)
local data = job.steps[job.currentStep]
local coords = data.coords
local label = data.label
local description = data.description
local anim = data.anim
local duration = data.duration
TriggerEvent("murphy_jobpanel:ShowObjective", description, 4000)
local blip = CreateBlip(coords, label, 'blip_objective', `BLIP_STYLE_OBJECTIVE`)
table.insert(blips, blip)
exports.murphy_interact:AddInteraction({
coords = vector3(coords.x, coords.y, coords.z + 1.5),
distance = 8.0,
interactDst = 2.5,
id = job.name .. "_step" .. job.currentStep,
options = {
{
label = label,
action = function(entity, coords, args)
local playerPed = PlayerPedId()
local startCoords = GetEntityCoords(playerPed)
if type(anim) == "string" then
TaskStartScenarioInPlace(playerPed, anim, -1, true)
elseif type(anim) == "table" then
LoadAnim(anim.dict)
TaskPlayAnim(playerPed, anim.dict, anim.anim, 8.0, 8.0, -1, 25, 0, 0, 0, 0)
end
Citizen.Wait(duration)
if #(GetEntityCoords(playerPed) - startCoords) > 10.0 then
ClearPedTasks(playerPed)
TriggerEvent("murphy_jobpanel:ShowObjective", "You moved too far, try again.", 4000)
else
ClearPedTasks(playerPed)
exports.murphy_interact:RemoveInteraction(job.name .. "_step" .. job.currentStep)
RemoveBlip(blip)
job:nextStep()
end
end,
},
}
})
end
actions["CompleteJob"] = function(job)
TriggerEvent("murphy_jobpanel:ShowObjective", lang["Finish"], 4000)
Callback.triggerServer('AddCurrency', function() end, job.steps[job.currentStep].reward)
job:nextStep()
Utils.StopJob()
end
function Utils.StopJob()
for _, blip in ipairs(blips) do
if blip then RemoveBlip(blip) end
end
blips = {}
end
function Utils.createJob(jobData)
for i, step in ipairs(jobData.steps) do
if step.action == "PlayAnimation" then
local location = orderedLocations[i]
step.coords = location.coords
step.anim = location.anim
step.duration = location.duration
step.description = location.description
step.label = location.name
end
step.action = actions[step.action]
end
return Job:new(jobData)
end
local job = Utils.createJob(JobData)
AddEventHandler("onResourceStop", function(resourceName)
if GetCurrentResourceName() ~= resourceName then return end
Utils.StopJob()
end)
return job
end

Step 3: Restart the resource

text
ensure murphy_jobpanel

That's it! Go to the Armadillo board and your new job will appear in the poster rotation.


Understanding the Job File Structure

Every job file has two main sections separated by if IsDuplicityVersion():

text
┌──────────────────────────────────────┐
│ JobData table (shared definition) │ <- Read by BOTH server and client
├──────────────────────────────────────┤
│ if IsDuplicityVersion() then │
│ Server: register in JobsConfig │ <- Server stores poster metadata
│ else │
│ Client: actions, Utils, logic │ <- Client runs the actual gameplay
│ end │
└──────────────────────────────────────┘

The JobData Table

Field Type Required Description
name string Yes Always use filename() - auto-generates a unique ID from the file name
label string Yes Display name shown on the poster
description string Yes Short description shown on the poster
steps table[] Yes Array of steps the player must complete (see below)
board string[] No Board names where this job appears. nil = all boards
image string Yes Path to the poster artwork image
reward string Yes Reward text displayed on the poster (display only)

Steps

Each step has at minimum:

Field Type Required Description
action string Yes Name of the action function to execute
label string No Text shown in the active job menu
description string No Objective text shown to the player
reward number No Money paid on this step (usually only on last step)

Additional fields depend on the action type (coords, vehicle, anim, etc.).

The board Field

The board field controls which physical boards in the world can display this job. Board names are defined in config/config.lua:

lua
Config.Panel = {
["armadillo"] = { ... },
["vanhorn"] = { ... },
["oil"] = { ... },
}
  • board = { "armadillo" } - only appears on the Armadillo board
  • board = { "armadillo", "vanhorn" } - appears on both boards
  • board = nil (or omit the field) - appears on all boards

Available Job Types

1. Task Job (Animations)

The simplest job type. The player goes to locations and performs animations (sweeping, cleaning, etc.).

Key concepts:

  • Each PlayAnimation step plays an animation at a specific location
  • Two animation formats: scenario (string) or custom anim (table)
  • Movement detection prevents AFK abuse
  • Ends with a CompleteJob step that pays the reward

Animation formats:

lua
-- Scenario (built-in game animation with props)
anim = "WORLD_HUMAN_BROOM"
-- Custom animation (dict + anim name)
anim = {
dict = "amb_work@world_human_clean_window@male_a@wip_base",
anim = "wip_base"
}

Duration is in milliseconds: duration = 5000 means 5 seconds.


2. Delivery Job (Ordered)

The player picks up a carriage loaded with packages, then delivers them one by one to specific locations.

Flow: Get carriage -> Deliver package 1 -> Deliver package 2 -> ... -> Return carriage

Key step types:

  • Initialisation - spawns the vehicle and packages
  • DeliverPackage - take a package from the carriage, carry it to a destination
  • ReturnCarriage - drive the empty carriage back to the start

Two variants exist:

  • Ordered (delivery_ordered.lua) - deliveries happen in a fixed sequence
  • Random (delivery_random.lua) - delivery locations are randomized each time

3. Pickup Job

The reverse of a delivery job. The player drives an empty carriage to various locations, picks up packages from the ground, loads them onto the carriage, then returns.

Flow: Get carriage -> Pick up package 1 -> Pick up package 2 -> ... -> Return carriage


4. Animal Hunting Job

The player travels to hunting zones and kills groups of animals.

Flow: Hunt pack 1 (e.g., 3 wolves) -> Hunt pack 2 (e.g., 2 cougars) -> Collect reward

Key concepts:

  • Animals spawn when the player gets within 200 units of the zone
  • Uses CreateBlipRadius to show a hunting area on the map
  • Animals are set to be aggressive toward the player
  • The step auto-completes when all animals in the group are dead

5. Cattle Driving Job

The player herds a group of cows from point A to point B through multiple waypoints.

Flow: Go to cows -> Herd to waypoint 1 -> Herd to waypoint 2 -> ... -> Complete

Key concepts:

  • Cows follow navmesh roads toward the destination
  • If a cow strays too far from the player, it stops and waits
  • Cows have a small random chance to wander off (player must keep them in line)
  • Dead cows reduce the final reward (malusfordeadcow setting)

6. Item Delivery Job

The player must have specific items in their inventory and deliver them to NPCs that spawn in the world.

Flow: Deliver item to NPC 1 -> Deliver item to NPC 2 -> ... -> Complete

Key concepts:

  • Requires items to already be in the player's inventory
  • NPCs spawn at delivery locations and wander in a radius
  • NPCs play a greeting animation and speech when the item is delivered
  • Items are removed from inventory on delivery via server callbacks

Setting Up Job Boards

Job boards are the physical objects in the world where players browse available jobs. Configure them in config/config.lua:

lua
Config.Panel = {
["my_board_name"] = {
model = "mp005_p_mp_bountyboard01x", -- Board prop model
coords = vector3(x, y, z), -- World position
rotation = vector3(0.0, 0.0, 90.0) -- Rotation (usually just Z axis)
},
}

Tips:

  • Use a unique, descriptive name as the key (e.g., "valentine", "rhodes", "strawberry")
  • The model should be a valid prop model hash - mp005_p_mp_bountyboard01x is the default bounty board
  • To find good coordinates, use the in-game dev tools or a coordinate finder resource

Auto-refresh: Boards refresh their poster selection every Config.Refreshpanels minutes (default: 60). Set to false to disable.


Customizing Poster Appearance

Each job has a poster image shown on the board UI. Available built-in images:

Image Path Theme
./img/Poster_art/beef.png Beef/meat
./img/Poster_art/bread.png Bread/bakery
./img/Poster_art/camp.png Camp/outdoors
./img/Poster_art/cow.png Cattle
./img/Poster_art/delivery.png Delivery wagon
./img/Poster_art/hunt1.png Hunting 1
./img/Poster_art/hunt2.png Hunting 2
./img/Poster_art/pig.png Pig/livestock
./img/Poster_art/piss.png Tavern
./img/Poster_art/train.png Train 1
./img/Poster_art/train2.png Train 2
./img/Poster_art/wagon.png Wagon
./img/Poster_art/water.png Water/river

To add custom images, place your .png file in ui/img/Poster_art/ and reference it in the job's image field.


Translations

In-Game Text (Lua side)

Each job file has its own lang table for step-specific text:

lua
local lang = {
["Take"] = "Take",
["Drop"] = "Drop",
["Finish"] = "Finish",
}

Change the values to translate to your language.

Global Prompts

Edit config/translate.lua:

lua
Translate = {
["Job Panel: View Jobs"] = "View Jobs",
["Already have a job"] = "You already have a job",
["Abandon"] = "You have abandoned the job",
}

UI Text

Edit ui/language_config.json:

json
[
{
"Close_button": "Close",
"Accept_button": "Accept"
},
{
"Header_title": "ACTIVE JOB",
"Objectives_title": "Objectives",
"Reward_text": "Reward",
"Abandon_button": "Abandon"
}
]

Frequently Asked Questions

How many jobs can I create?

There is no hard limit. Each .lua file in config/jobs/ is a separate job. The board randomly displays 6 posters at a time from the available pool.

Can I restrict a job to certain boards?

Yes. Use the board field: board = { "valentine", "rhodes" }. Omit it to show on all boards.

Can a player do multiple jobs at once?

No. A player can only have one active job at a time. If they already have a job, the UI shows "You already have a job".

How does job persistence work?

Job progress is saved to the murphy_jobpanel database table. If the server restarts or the player disconnects, they resume from their last completed step.

How do I change the framework?

Edit Config.framework in config/config.lua:

lua
Config.framework = "vorp" -- Options: vorp, rsg-core, REDEMRP2k23, redemrp2022, qbr-core

What happens when a player abandons a job?

All spawned entities (vehicles, packages, animals, NPCs) are cleaned up, blips are removed, and the database record is cleared. There is a 30-minute cooldown before the job poster reappears on the board.


Last updated: September 9, 2026