RedM documentation
Murphy's Job Board
Advanced
Create a job - Advanced Guide
Deep dive into the job system architecture, custom action development, and advanced patterns.
Table of Contents
- Architecture Overview
- Job Lifecycle
- Writing Custom Actions
- The Callback System
- Working with Entities
- Interaction System (murphy_interact)
- Blips and GPS Routes
- Ordered vs Random Patterns
- Inventory Integration
- Relationship Groups (Combat/Herding)
- The Utils Pattern
- Building a Complete Custom Job
- Framework Adapters
- Database and Persistence
- Performance Considerations
- Debugging Tips
Architecture Overview
Key insight: Job files run on both server and client. The IsDuplicityVersion() check splits the logic:
- Server side only stores the job's metadata in
JobsConfig(for poster generation) - Client side contains all gameplay logic: actions, entity spawning, interactions, cleanup
Job Lifecycle
Resuming After Disconnect
When a player connects, the GetProgression server callback checks the DB for an existing record. If found, the client receives (jobName, stepNumber) and calls Job:start(step) to resume from where they left off.
Writing Custom Actions
Action Function Signature
Every action receives the job object as its only parameter:
The job object exposes:
| Property/Method | Type | Description |
|---|---|---|
job.name |
string | Unique job identifier (from filename()) |
job.label |
string | Display name |
job.steps |
table[] | All step definitions |
job.currentStep |
number | Index of the currently executing step (1-based) |
job.completed |
boolean | True when all steps are done |
job.abandon |
boolean | True when the player abandoned the job |
job.activejobinfo |
table | Data sent to the active job UI overlay |
job:start(step?) |
function | Start the job (optionally at a specific step) |
job:stop() |
function | Abandon the job |
job:nextStep() |
function | Advance to the next step |
job:executeCurrentStep() |
function | Run the current step's action |
The Abandon Watcher Pattern
Every action that spawns entities or creates interactions must include an abandon watcher thread. This is the standard cleanup pattern used across all job types:
Why this works: The watcher captures job.currentStep at the time the action starts. When job:nextStep() increments it, step ~= job.currentStep becomes true, triggering cleanup. If the player abandons, job.abandon is set to true by Job:stop().
Complete Example: Custom Action
Here's a fishing-spot action where the player must stand near water and play a fishing animation:
The Callback System
Murphy JobPanel uses a custom lightweight RPC system (not framework callbacks) defined in config/Callbacks.lua.
Client -> Server
Server -> Client
Available Server Callbacks
These are pre-registered and ready to use in your actions:
| Callback Name | Arguments | Returns | Description |
|---|---|---|---|
AddCurrency |
amount |
- | Add money to the player |
RemoveCurrency |
amount |
- | Remove money from the player |
GetCharMoney |
- | number |
Get the player's cash balance |
GetItemAmount |
item |
number |
Count of an item in inventory |
GiveItem |
item, amount, meta |
boolean |
Add item to inventory |
RemoveItem |
item, amount, meta |
boolean |
Remove item from inventory |
GetCharJob |
- | job, grade |
Get player's current RP job |
GetCharIdentifier |
- | string |
Get character ID |
GetProgression |
- | job, step |
Load saved job progression |
RequestJobPosters |
boardName |
table[] |
Get available jobs for a board |
TakeJobPoster |
jobName |
boolean |
Claim a job from the board |
Usage example (paying the player):
Usage example (checking inventory):
Working with Entities
Spawning Vehicles
Important: Always call SetEntityAsMissionEntity to prevent the game from despawning your vehicle.
Spawning Peds/Animals
For NPCs that shouldn't die:
For animals that wander:
Attaching Objects
Objects can be attached to vehicles or peds using the native 0x6B9BBD38AB0796DF (AttachEntityToEntity):
Attach to vehicle (packages on a carriage):
Attach to ped (carrying a box):
Interaction System (murphy_interact)
The job system uses murphy_interact for player-to-world interactions. Three types are available:
Static World Interaction
Entity Interaction (networked)
Local Entity Interaction (client-only)
Best practice for IDs: Use job.name .. "_step" .. job.currentStep to ensure unique IDs that can be cleaned up per-step.
Blips and GPS Routes
Creating Blips
GPS Route
Draw a GPS route line on the minimap:
Blip Styles
Common blip styles used in the resource:
| Hash | Usage |
|---|---|
`BLIP_STYLE_OBJECTIVE` |
Standard objective |
`BLIP_STYLE_NEUTRAL` |
Neutral/waiting |
`BLIP_STYLE_MP_MISSION_GIVER` |
Warning/attention |
`BLIP_STYLE_HERDING_ANIMAL` |
Herding |
Always Clean Up Blips
Track all blips in a local table and remove them in Utils.StopJob():
Ordered vs Random Patterns
Ordered Pattern
Locations are assigned to steps sequentially in Utils.createJob():
The player always visits locations in the same order.
Random Pattern
Locations are randomly assigned, with deduplication to avoid repeats:
Note for random delivery jobs: The NewLocation() function also randomizes at runtime (not just at creation), so each job run gives a different route.
Rule of thumb: Make sure you have more locations in the deliveries table than DeliverPackage steps, otherwise the random selection loop will hang.
Inventory Integration
For jobs that interact with the player's inventory, use the server callbacks:
Check if player has items
Remove items from player
Give items to player
Relationship Groups (Combat/Herding)
Aggressive Animals (Hunting)
Make animals hostile toward the player:
Passive Animals (Herding)
Make animals neutral but responsive to player proximity:
The Utils Pattern
Every job file follows the same organizational pattern with a Utils table:
The three required functions:
Utils.StopJob()- cleans up all entities and stateUtils.createJob(jobData)- maps location data to steps and creates the Job instanceonResourceStophandler - cleanup when resource is restarted
Building a Complete Custom Job
Here's a full example of a Mining Job where the player goes to mine locations, plays mining animations, and collects ore:
Framework Adapters
The adapter layer abstracts framework-specific APIs so job logic stays framework-agnostic. All adapters are in server/adapters/ and are guarded by:
Adapter Interface
Every adapter implements these functions (called via server callbacks):
| Function | Purpose |
|---|---|
GetCharIdentifier(id) |
Get character's unique identifier |
GetCharFirstname(id) |
Get character's first name |
GetCharLastname(id) |
Get character's last name |
GetCharMoney(id) |
Get cash balance |
AddCurrency(id, amt) |
Add cash |
RemoveCurrency(id, amt) |
Remove cash |
GetItemAmount(src, item) |
Get item count in inventory |
GiveItem(src, item, amt, meta) |
Add item to inventory |
RemoveItem(src, item, amt, meta) |
Remove item from inventory |
GetCharJob(id) |
Get current job name |
GetCharJobGrade(id) |
Get current job grade |
GetCharGroup(id) |
Get permission group (admin, etc.) |
Adding Support for a New Framework
- Create
server/adapters/MyFrameworkAdapter.lua - Wrap all code in
if Config.framework == "myframework" then - Implement all functions listed above using your framework's APIs
- Add the framework name to
Config.frameworkoptions inconfig.lua
Database and Persistence
Schema
How Persistence Works
- On job start: Inserts/updates
(charid, jobName, 1) - On step advance: Updates
stepcolumn - On complete/abandon: Sets
jobandsteptoNULL - On player connect: Queries by
charidto check for in-progress job
This is handled automatically by the SaveProgression event and GetProgression callback - you don't need to interact with the DB directly in job files.
Performance Considerations
Wait intervals: Use
Wait(1000)for monitoring threads (abandon watchers, arrival checks). Only useWait(0)when absolutely necessary (animation loops).Wait(200)is fine for proximity checks.Model loading: Always pair
RequestModelwithSetModelAsNoLongerNeededafter spawning to free memory.Entity cleanup: Every entity you create must be deleted in
Utils.StopJob()and in theonResourceStophandler. Leaked entities persist in the game world.Blip cleanup: Same as entities - track all blips in a table and remove them on cleanup.
Interaction cleanup: Always call
RemoveInteraction/RemoveEntityInteraction/RemoveLocalEntityInteractionwhen a step ends or the job is abandoned.Board refresh:
Config.Refreshpanels = 60regenerates posters every 60 minutes. Lower values increase server load slightly. Set tofalseif you don't need auto-refresh.
Debugging Tips
Print Statements
Lua print() outputs to the server or client console depending on context. Use it freely for development:
Common Issues
| Problem | Likely Cause | Fix |
|---|---|---|
| Job doesn't appear on board | board field doesn't match any Config.Panel key |
Check spelling matches exactly |
| Actions don't execute | String name in step.action doesn't match actions["Name"] |
Verify exact string match |
| Entities don't spawn | Model not loaded or coords underground | Add HasModelLoaded wait loop; check Z coordinate |
| Job gets stuck | job:nextStep() never called |
Ensure all code paths call nextStep() |
| Entities persist after abandon | Missing abandon watcher or StopJob() doesn't delete them |
Add watcher thread; check cleanup function |
| "Already have a job" | Player has a stale DB record | Check murphy_jobpanel table, clear the row |
| GPS route doesn't clear | ClearGpsMultiRoute() missing from cleanup |
Add to both step cleanup and StopJob() |
Checking DB State
Last updated: September 9, 2026