🤠 Wild Country Libs
Guide

Creating A Simple Job

This walks through a complete, minimal job resource — an NPC prompt, a start event, server-side validation, a reward, a notification, and cleanup — using nothing but wc_libs. By the end you'll have a working wc_stagecoach-style job you can copy and adapt.

Use this when you're building any "walk up to an NPC, do a thing, get paid" job — deliveries, errands, patrols, or contracts. This is the shape almost every Green Studio job resource follows.

What you'll need

  1. Declare the dependency

    Every wc_libs resource needs this in its manifest so the wc proxy is available and the resource fails loudly if wc_libs isn't running.

    wc_stagecoach/fxmanifest.lua
    fx_version 'adamant'
    game 'rdr3'
    
    name 'wc_stagecoach'
    dependency 'wc_libs'
    
    shared_scripts { '@wc_libs/init.lua' }
    client_scripts { 'client/main.lua' }
    server_scripts { 'server/main.lua' }
  2. Spawn the NPC and create a prompt

    Spawn the clerk once on resource start, and use WatchPrompt to show a "[G] Buy Ticket" prompt only while the player is nearby — it handles the range-check loop and prompt visibility for you.

    wc_stagecoach/client/main.lua
    local clerkPed
    
    CreateThread(function()
      local hash = wc:LoadModel("a_m_m_hillfolk_01")
      clerkPed = wc:SpawnPed(hash, -290.5, 800.2, 118.9, 160.0, true, true)
      wc:CreateBlip(-290.5, 800.2, 118.9, "Stagecoach Station")
    
      local prompt = wc:CreatePrompt("Buy Ticket [G]")
      wc:WatchPrompt(prompt, clerkPed, 2.5, function()
        TriggerServerEvent('wc_stagecoach:start')
      end)
    end)
  3. Fire a start event — client asks, server decides

    The client only asks to start the job. All the real decisions (is a run already active, can the player afford it, are they in the right job) happen server-side.

    wc_stagecoach/server/main.lua
    local ActiveRuns = {}
    
    RegisterNetEvent('wc_stagecoach:start', function()
      local src = source
    
      -- server validation: no double-starts
      if ActiveRuns[src] then
        wc:Notify(src, { variant = 'fail', title = "You already have a run active." })
        return
      end
    
      ActiveRuns[src] = { reward = 50, startedAt = os.time() }
      TriggerClientEvent('wc_stagecoach:begin', src, ActiveRuns[src])
    end)
    Security. Never trust a client-sent reward amount. The server decides reward = 50 here and remembers it in ActiveRuns — the client never gets a chance to influence the payout.
  4. Send the player on their way

    On the client, react to the server's begin event by dropping a destination blip and GPS route with CreateMissionMarker.

    wc_stagecoach/client/main.lua
    local bag = wc:CreateCleanupBag()
    
    RegisterNetEvent('wc_stagecoach:begin', function(run)
      local dest = vector3(273.5, -1258.0, 67.0) -- Valentine
    
      bag:AddBlip(wc:CreateMissionMarker(dest.x, dest.y, dest.z, "Delivery Point", { route = true }))
      bag:AddGPS()
      wc:TopNotify('Stagecoach Run', 'Deliver the mail sack to Valentine')
    
      wc:WatchPlayerNear(dest, 5.0, function()
        bag:Clean()
        TriggerServerEvent('wc_stagecoach:complete')
      end, { timeoutMs = 600000 })
    end)
  5. Validate completion and pay the reward

    The server re-checks ActiveRuns[src] exists before paying anyone — a player who never actually started a run (or is replaying an old client event) gets nothing.

    wc_stagecoach/server/main.lua
    RegisterNetEvent('wc_stagecoach:complete', function()
      local src = source
      local run = ActiveRuns[src]
      if not run then return end -- no active run — ignore
    
      ActiveRuns[src] = nil
    
      wc:AddMoney(src, run.reward, 0)
      wc:Notify(src, { title = ("Stagecoach Complete: +$%d"):format(run.reward), variant = 'avanced' })
    end)
  6. Clean up on disconnect

    If the player disconnects mid-run, remove their entry so it doesn't leak memory or block a future run. See the Safe Resource Cleanup guide for the full pattern.

    wc_stagecoach/server/main.lua
    AddEventHandler('playerDropped', function()
      ActiveRuns[source] = nil
    end)

Common mistakes