FiveM Docs
Getting Started

Your First ESX Resource

Build your first ESX Legacy resource step by step - fxmanifest with imports.lua, a server command with xPlayer, client notifications and a server callback.

This tutorial builds a small but complete ESX resource from scratch: a /balance command that shows a player's money, and a /buywater command that charges the player on the server and gives them an item. Along the way you learn the pieces almost every ESX script uses: the manifest, the ESX object, xPlayer, commands, notifications and server callbacks.

You need a running ESX Legacy server. If you do not have one yet, follow Installation first.

What we build

FeatureSideESX API used
/balance shows cash and bankserverESX.RegisterCommand, xPlayer.getMoney, xPlayer.getAccount, xPlayer.showNotification
/buywater buys a water bottleclient + serverESX.TriggerServerCallback, ESX.RegisterServerCallback, xPlayer.removeAccountMoney, xPlayer.addInventoryItem
Welcome message on spawnclientesx:playerLoaded, ESX.ShowNotification

Set up your editor

Any editor works, but Visual Studio Code with these extensions catches most mistakes before you restart the server:

  • Lua by sumneko: the Lua language server (autocomplete, diagnostics).
  • CfxLua IntelliSense by overextended: FiveM natives and Lua 5.4 syntax used by FiveM.
  • ESX VSCode: snippets and completion for ESX functions.

If the Lua extension reports globals like ESX or CreateThread as undefined, add this to your VS Code settings:

.vscode/settings.json
{
  "Lua.diagnostics.disable": ["undefined-global"],
  "Lua.diagnostics.globals": ["ESX", "Config"]
}

Folder structure

Create a folder my_first_script inside your resources folder (or a category folder such as resources/[local]).

fxmanifest.lua
config.lua
main.lua
main.lua

Build it

Write the manifest

fxmanifest.lua tells FiveM which files to load and where.

fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

name 'my_first_script'
author 'Your Name'
version '1.0.0'

shared_scripts {
    '@es_extended/imports.lua', -- defines the global ESX on client and server
    'config.lua',
}

client_scripts {
    'client/main.lua',
}

server_scripts {
    'server/main.lua',
}

dependency 'es_extended'
  • @es_extended/imports.lua runs ESX = exports['es_extended']:getSharedObject() inside your resource. On the client it also keeps ESX.PlayerData up to date and sets ESX.PlayerLoaded.
  • lua54 'yes' enables Lua 5.4. All ESX core resources use it.
  • dependency 'es_extended' makes FiveM refuse to start your script without ESX, instead of failing later with nil errors.
  • Load imports.lua before your own files, otherwise ESX is nil while they run.

Add a config file

config.lua
Config = {}

Config.WaterPrice = 25        -- price in cash
Config.WaterItem = 'water'    -- item name from the items table

The config is shared, so the client sees it too. That is fine for prices, but never put webhooks, API keys or other secrets in a shared file. Put them in a server-only file instead.

Write the server side

server/main.lua
-- /balance: show cash and bank to the player who typed it
ESX.RegisterCommand('balance', 'user', function(xPlayer, args, showError)
    if not xPlayer then
        return print('This command can only be used in game.')
    end

    local cash = xPlayer.getMoney()
    local bank = xPlayer.getAccount('bank')

    xPlayer.showNotification(('Cash: $%s | Bank: $%s'):format(
        ESX.Math.GroupDigits(cash),
        ESX.Math.GroupDigits(bank and bank.money or 0)
    ), 'info')
end, false, { help = 'Show your cash and bank balance' })

-- Callback used by /buywater. The client asks, the server decides.
ESX.RegisterServerCallback('my_first_script:buyWater', function(source, cb)
    local xPlayer = ESX.GetPlayerFromId(source)
    if not xPlayer then
        return cb(false, 'Player not loaded')
    end

    if not xPlayer.canCarryItem(Config.WaterItem, 1) then
        return cb(false, 'You cannot carry more')
    end

    -- removeAccountMoney returns false if the player cannot afford it
    if not xPlayer.removeAccountMoney('money', Config.WaterPrice, 'Bought water') then
        return cb(false, 'Not enough cash')
    end

    xPlayer.addInventoryItem(Config.WaterItem, 1)
    cb(true)
end)

Things to notice:

  • ESX.RegisterCommand(name, group, cb, allowConsole, suggestion) passes the xPlayer of the caller to your function. Because allowConsole is false, the console cannot run it, but the xPlayer check is still a good habit: from the console xPlayer is false.
  • The second argument is the group that may use the command. ESX commands are always restricted, so with 'admin' normal players would get "access denied". Use 'user' for everyone or a table like { 'admin', 'owner' }.
  • The callback receives source first, then the function cb to answer with, then any extra arguments the client sent.
  • All money logic happens on the server. The client only asks; it never says how much to pay.

Write the client side

client/main.lua
-- Runs once when the player's character has loaded
RegisterNetEvent('esx:playerLoaded', function(xPlayer)
    ESX.ShowNotification(('Welcome back, %s!'):format(xPlayer.firstName or GetPlayerName(PlayerId())), 'success')
end)

-- /buywater: ask the server to buy a water bottle
RegisterCommand('buywater', function()
    if not ESX.IsPlayerLoaded() then return end

    ESX.TriggerServerCallback('my_first_script:buyWater', function(success, reason)
        if success then
            ESX.ShowNotification(('You bought water for $%d'):format(Config.WaterPrice), 'success')
        else
            ESX.ShowNotification(reason or 'Purchase failed', 'error')
        end
    end)
end, false)

TriggerEvent('chat:addSuggestion', '/buywater', ('Buy a bottle of water for $%d'):format(Config.WaterPrice))

ESX.ShowNotification(message, type, length, title, position) uses esx_notify. Valid types are info, success and error.

If you prefer code without nested functions, ESX also has an awaitable version that must run inside a thread or command handler:

client/main.lua
RegisterCommand('buywater2', function()
    local success, reason = ESX.AwaitServerCallback('my_first_script:buyWater')
    ESX.ShowNotification(success and 'Bought water' or reason, success and 'success' or 'error')
end, false)

Start and test

Add the resource to server.cfg below the ESX lines:

server.cfg
ensure es_extended
ensure [core]
ensure [esx_addons]
ensure my_first_script

Then start it without restarting the server: type ensure my_first_script in the server console or txAdmin live console. In game, type /balance, then /buywater and open your inventory (F2 by default).

After every change, run ensure my_first_script again (or restart my_first_script).

Getting the player on the server

Most server code starts by turning a source into an xPlayer:

server/main.lua
RegisterNetEvent('my_first_script:doSomething', function()
    local xPlayer = ESX.GetPlayerFromId(source)
    if not xPlayer then return end -- not loaded yet or already dropped

    print(xPlayer.getIdentifier(), xPlayer.getName(), xPlayer.job.name, xPlayer.getGroup())
end)

ESX.GetPlayerFromId returns nil until the player has picked a character, so always check the result. The full list of methods is in the xPlayer reference.

Security basics for your first script

  • Never trust the client. A cheater can trigger any RegisterNetEvent with any arguments. Prices, amounts, item names and target IDs must be decided or validated on the server.
  • Check distance and job on the server before rewarding something, for example with xPlayer.getCoords(true) and xPlayer.job.name.
  • Prefer callbacks over "give me X" events. A server event named my_script:giveMoney that takes an amount is the classic exploit.

Common mistakes

SymptomCause
attempt to index a nil value (global 'ESX')@es_extended/imports.lua missing in the manifest, or listed after your files, or your resource starts before es_extended.
attempt to call a nil value (field 'RegisterCommand') on the clientESX.RegisterCommand is server-only. Use the native RegisterCommand on the client.
Command says "access denied"Wrong group in ESX.RegisterCommand, or the resource.es_extended ACE lines are missing from server.cfg.
Callback never answersThe name differs between client and server, or the server function returned without calling cb.
Item "water" was used but does not existThe item is not in the items table. See Add items.

Next steps

Frequently asked questions

Independent documentation

esx-fivem.com is a community project and is not affiliated with the ESX Framework team. Facts on this page were checked against the upstream source. Official resources: docs.esx-framework.org and github.com/esx-framework.
Edit on GitHub

Last updated on

On this page