Tutorial 2026-04-30

Creating an Ambulance Job Script for FiveM

TDYSKY

TDYSKY

Founder & Lead Developer at Agency Scripts

Ambulance Job System Overview

An ambulance job script is essential for any roleplay server that aims for immersive emergency services gameplay. The EMS system handles everything from receiving dispatch calls when a player goes down, to on-scene treatment mechanics, hospital transport, and medical billing. A well-designed ambulance script creates meaningful gameplay for EMS players while ensuring downed players are not stuck waiting indefinitely. The architecture splits into four major subsystems: the dispatch and notification system that alerts EMS to incidents, the treatment and stabilization mechanics that give medics actual tasks to perform, the hospital check-in and recovery process, and the billing system that creates an economic cost for injuries. Each subsystem needs to be independently functional so that if no EMS players are on duty, automatic fallback systems handle player recovery.

Job Configuration and Duty System

The ambulance job needs clearly defined grades with different capabilities at each level. Trainees might only be able to perform basic first aid, while senior paramedics can administer advanced treatments and access restricted medical supplies. Use a configuration table that maps job grades to available actions, required items, and payment rates. The duty system should track which EMS players are currently active so the server knows whether to use the automatic respawn fallback:

Config.AmbulanceJob = {
    name = 'ambulance',
    grades = {
        [0] = {
            label = 'Trainee',
            treatments = {'bandage', 'painkillers'},
            canRevive = false,
            salary = 250,
        },
        [1] = {
            label = 'EMT',
            treatments = {'bandage', 'painkillers', 'splint', 'iv_drip'},
            canRevive = true,
            salary = 400,
        },
        [2] = {
            label = 'Paramedic',
            treatments = {'bandage', 'painkillers', 'splint', 'iv_drip',
                         'defibrillator', 'blood_transfusion'},
            canRevive = true,
            salary = 550,
        },
        [3] = {
            label = 'Chief',
            treatments = {'all'},
            canRevive = true,
            salary = 750,
        },
    },
    minOnDutyForNoAutoRespawn = 2,
    respawnTimer = 300,  -- seconds before auto-respawn when no EMS
    hospitalCoords = vector3(311.2, -584.3, 43.3),
    bedSpawns = {
        vector4(309.7, -583.8, 43.3, 70.0),
        vector4(313.0, -585.2, 43.3, 70.0),
        vector4(316.3, -586.6, 43.3, 70.0),
    },
}

The minOnDutyForNoAutoRespawn setting controls when the server switches between EMS-dependent and automatic recovery. When fewer than two EMS are on duty, downed players get a timer-based respawn option after waiting the configured number of seconds. This prevents players from being stuck waiting for medical attention that will never arrive.

Dispatch and Notification System

When a player is incapacitated, the system broadcasts a dispatch alert to all on-duty EMS players showing the location, nature of the emergency, and a way to claim the call. Multiple EMS players can see the alert, but only one should claim it to avoid confusion. Implement a claim system where the first responder to accept the call gets it assigned, and other EMS see it as claimed with the responding unit's identifier:

-- Server: handle player down event
local ActiveCalls = {}

RegisterNetEvent('ambulance:server:playerDown', function(deathCause)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local ped = GetPlayerPed(src)
    local coords = GetEntityCoords(ped)
    local streetHash, _ = GetStreetNameAtCoord(coords.x, coords.y, coords.z)
    local streetName = GetStreetNameFromHashKey(streetHash)

    local callId = 'EMS-' .. os.time() .. '-' .. src
    ActiveCalls[callId] = {
        source = src,
        patient = Player.PlayerData.charinfo.firstname .. ' ' ..
                  Player.PlayerData.charinfo.lastname,
        coords = coords,
        street = streetName,
        cause = deathCause,
        time = os.time(),
        claimed = false,
        responder = nil,
    }

    -- Notify all on-duty EMS
    local emsList = QBCore.Functions.GetPlayersOnDuty('ambulance')
    for _, emsId in ipairs(emsList) do
        TriggerClientEvent('ambulance:client:newCall', emsId, callId, ActiveCalls[callId])
    end
end)

RegisterNetEvent('ambulance:server:claimCall', function(callId)
    local src = source
    local call = ActiveCalls[callId]
    if not call or call.claimed then return end

    local Player = QBCore.Functions.GetPlayer(src)
    if not Player or Player.PlayerData.job.name ~= 'ambulance' then return end

    call.claimed = true
    call.responder = src

    -- Notify all EMS that call is claimed
    local emsList = QBCore.Functions.GetPlayersOnDuty('ambulance')
    for _, emsId in ipairs(emsList) do
        TriggerClientEvent('ambulance:client:callClaimed', emsId, callId, Player.PlayerData.charinfo.firstname)
    end

    -- Set GPS waypoint for responding EMT
    TriggerClientEvent('ambulance:client:setWaypoint', src, call.coords)
end)

Include the death cause in the dispatch information so the responding EMT can prepare appropriate treatment items before arriving on scene. Common causes like gunshot wounds, vehicle collisions, falls, and drowning each require different treatment approaches, which adds depth to the EMS roleplay experience.

Treatment and Revive Mechanics

Treatment mechanics should be more involved than simply pressing a button to revive someone. Create a multi-step process where the EMT must first stabilize the patient with basic treatments before performing the actual revival. Each treatment step consumes an item from the EMT's inventory and takes a set amount of time with a progress bar animation. The patient's condition can be modeled with a simple health state that improves with each treatment applied:

-- Client: treatment system
local TreatmentSteps = {
    ['bandage'] = {
        label = 'Apply Bandage',
        duration = 5000,
        anim = {dict = 'mini@cpr@char_a@cpr_str', name = 'cpr_pumpchest'},
        healthRestore = 10,
        item = 'bandage',
    },
    ['painkillers'] = {
        label = 'Administer Painkillers',
        duration = 3000,
        anim = {dict = 'mp_arresting', name = 'a_uncuff'},
        healthRestore = 15,
        item = 'painkillers',
    },
    ['defibrillator'] = {
        label = 'Use Defibrillator',
        duration = 8000,
        anim = {dict = 'mini@cpr@char_a@cpr_str', name = 'cpr_pumpchest'},
        healthRestore = 0,
        item = 'defibrillator',
        canRevive = true,
    },
}

function PerformTreatment(targetId, treatmentType)
    local treatment = TreatmentSteps[treatmentType]
    if not treatment then return end

    -- Check if EMT has required item
    if not HasItem(treatment.item) then
        QBCore.Functions.Notify('Missing: ' .. treatment.label, 'error')
        return
    end

    -- Play animation and progress bar
    TaskPlayAnim(PlayerPedId(), treatment.anim.dict, treatment.anim.name,
        8.0, -8.0, treatment.duration, 1, 0, false, false, false)

    QBCore.Functions.Progressbar('treatment_' .. treatmentType,
        treatment.label, treatment.duration, false, true, {}, {}, {}, {},
        function() -- success
            TriggerServerEvent('ambulance:server:applyTreatment',
                targetId, treatmentType)
            ClearPedTasks(PlayerPedId())
        end,
        function() -- cancel
            ClearPedTasks(PlayerPedId())
            QBCore.Functions.Notify('Treatment cancelled', 'error')
        end
    )
end

The progress bar with animation creates a realistic treatment scene that other players can observe, enhancing the roleplay atmosphere. Make the defibrillator the final step that actually performs the revival, requiring the EMT to first stabilize the patient with bandages and painkillers. This multi-step approach makes the EMS role more engaging than a simple one-click revive.

Hospital Check-In and Recovery

After reviving a patient in the field or transporting them to the hospital, an EMS player can check them into a hospital bed for full recovery. The check-in process heals the patient completely, charges a medical bill, and logs the visit. Hospital beds should be tracked to prevent multiple patients from being assigned to the same bed. Create a bed management system that marks beds as occupied and releases them after a configurable recovery time:

-- Server: hospital bed management
local OccupiedBeds = {}

RegisterNetEvent('ambulance:server:checkInPatient', function(patientId)
    local src = source
    local EMT = QBCore.Functions.GetPlayer(src)
    local Patient = QBCore.Functions.GetPlayer(patientId)

    if not EMT or not Patient then return end
    if EMT.PlayerData.job.name ~= 'ambulance' then return end

    -- Find available bed
    local bedIndex = nil
    for i, bed in ipairs(Config.AmbulanceJob.bedSpawns) do
        if not OccupiedBeds[i] then
            bedIndex = i
            break
        end
    end

    if not bedIndex then
        TriggerClientEvent('QBCore:Notify', src, 'No beds available', 'error')
        return
    end

    OccupiedBeds[bedIndex] = patientId

    -- Calculate and charge medical bill
    local bill = CalculateMedicalBill(Patient)
    Patient.Functions.RemoveMoney('bank', bill, 'medical-bill')

    -- Heal patient and teleport to bed
    local bed = Config.AmbulanceJob.bedSpawns[bedIndex]
    TriggerClientEvent('ambulance:client:bedRecovery', patientId, bed, bill)

    -- Pay EMT for service
    local salary = Config.AmbulanceJob.grades[EMT.PlayerData.job.grade.level].salary
    EMT.Functions.AddMoney('bank', salary, 'ems-treatment-pay')

    -- Release bed after recovery
    SetTimeout(60000, function()
        OccupiedBeds[bedIndex] = nil
    end)
end)

function CalculateMedicalBill(Patient)
    local baseCost = 500
    local injuryMultiplier = 1.0
    -- Could factor in injury severity, treatment count, etc.
    return math.floor(baseCost * injuryMultiplier)
end

The medical bill creates an economic consequence for dying that balances the server economy. Consider scaling the bill based on how the player was injured, with higher costs for reckless behavior like high-speed crashes versus lower costs for being a crime victim. This encourages careful driving and adds weight to dangerous situations.

Auto-Respawn Fallback System

When no EMS players are on duty, the server must provide an alternative recovery path so downed players are not permanently stuck. Implement a countdown timer that appears after a configurable delay, allowing the player to respawn at the hospital with a flat medical fee deducted automatically. The respawn location should be at the hospital entrance or a designated recovery area. Always check the current EMS count before showing the auto-respawn option, and dismiss it immediately if an EMT claims the call during the countdown. This dual-path system ensures players always have a way to recover while still prioritizing the roleplay experience of EMS interaction when medics are available.

Medical Supplies and Inventory Integration

EMS players need access to medical supplies through a job-restricted stash at the hospital and through a crafting or purchasing system. Stock the hospital stash with bandages, painkillers, splints, IV drips, blood bags, and defibrillator charges. Each treatment action consumes the corresponding item, creating ongoing demand for resupply. Track supply usage in the database to monitor consumption patterns and automatically restock hospital stashes at configurable intervals. Consider making some supplies craftable by players with the pharmacist or doctor role, creating additional roleplay opportunities and economic connections between different jobs on the server.

Logging and Performance Monitoring

Log every EMS interaction for administrative review, including who was treated, what treatments were applied, the responding EMT, and the final medical bill. Store these logs in a dedicated database table and optionally send summaries to a Discord webhook for real-time staff monitoring. These logs help resolve disputes, identify EMS players who are not performing their duties, and track overall system health. On the performance side, keep the death check logic lightweight by only running it on players whose health has dropped to zero, not on every player every frame. Use event-driven architecture where the death state triggers a single server event rather than polling. The dispatch notification system should batch alerts rather than sending individual events to each EMS player, reducing network overhead during mass-casualty scenarios where multiple players go down simultaneously.

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.