Why ChatGPT Writes Broken FiveM Scripts (and How to Fix It)

8 min read

  • chatgpt fivem
  • ai fivem lua
  • skills

ChatGPT writes broken FiveM scripts because it learned FiveM from forum posts and GitHub repos spanning ten years of changing APIs, with no way to tell which version you run. The result is code that mixes ESX and QBCore calls, uses natives that do not exist, puts database queries on the client, and trusts every value a player sends. The fix is not a better prompt. It is giving the model the actual documentation for your stack in a format it reads before it writes: a skill.

The six failure patterns in AI-written FiveM Lua

After reviewing a lot of AI-generated resources, the same six mistakes show up in almost every one. If you learn to spot them, you can review a script in minutes.

1. Invented natives

The model has seen thousands of native names, so it can generate plausible new ones. GetVehicleOwner(vehicle), SetPlayerJob(source, 'police'), IsPedInAnyOwnedVehicle. None of these exist. Real natives follow the naming on docs.fivem.net/natives, and a native that is not on that page is not a native.

The tell: a native that does exactly the high-level thing you asked for. Real natives are low-level. There is no native for “owner of a vehicle” because ownership is a framework concept stored in your database, not in the game engine.

2. Mixed QBCore, Qbox and ESX APIs

Ask for a QBCore script and you will often get ESX.GetPlayerFromId halfway through, or QBCore.Functions.GetPlayer next to exports.qbx_core:GetPlayer. The model does not know these are three different frameworks with three different player objects. It knows they are all “FiveM roleplay code” and blends them.

Before and after, for a server event that pays a player:

-- AI output: ESX call inside a QBCore resource
RegisterNetEvent('myjob:pay', function(amount)
    local xPlayer = ESX.GetPlayerFromId(source)
    xPlayer.addMoney(amount)
end)
-- Fixed for QBCore, with the reward decided on the server
local QBCore = exports['qb-core']:GetCoreObject()

RegisterNetEvent('myjob:pay', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end
    local reward = Config.Reward -- never trust an amount sent by the client
    Player.Functions.AddMoney('cash', reward, 'myjob-payout')
end)

Note the second fix in that example: the original accepted amount from the client. That is failure pattern five, and the two usually arrive together.

3. Client code where server code belongs (and the reverse)

FiveM runs two Lua environments. The client runs on the player’s PC and can draw UI, play animations and read the local ped. The server holds the database, the money and the truth. AI models blur this constantly:

  • MySQL.query in client.lua. Impossible; oxmysql is a server-only resource. It will also leak your connection string if it did work.
  • TriggerClientEvent called from a client script. That is a server function.
  • GetPlayerName(source) on the client, where source does not exist.
  • DrawText or lib.notify on the server, where there is no screen to draw on.

The tell: a single main.lua doing everything. Real resources separate client/ and server/ and the fxmanifest.lua says which file is which.

4. Missing or wrong fxmanifest fields

A manifest the model writes from memory often looks like this:

resource_manifest_version '44febabe-d386-4d18-afbe-5e627f4af937'
client_script 'client.lua'
server_script 'server.lua'

That is the 2018 format. It still loads, but it silently disables Lua 5.4 and newer manifest features. The current form is:

fx_version 'cerulean'
game 'gta5'
lua54 'yes'

shared_script '@ox_lib/init.lua'
client_script 'client.lua'
server_scripts {
    '@oxmysql/lib/MySQL.lua',
    'server.lua'
}

The most common omissions are lua54 'yes' (the script then fails on <const> or integer division), the @ox_lib/init.lua shared script (every lib. call becomes “attempt to index a nil value (global ‘lib’)”), and @oxmysql/lib/MySQL.lua (same error, MySQL this time).

5. Server events that trust the client

This is the one that gets servers robbed. A model writes:

RegisterNetEvent('shop:buy', function(item, price)
    local Player = QBCore.Functions.GetPlayer(source)
    Player.Functions.RemoveMoney('cash', price)
    Player.Functions.AddItem(item, 1)
end)

Any player with a cheat menu can trigger shop:buy with ('weapon_pistol', 0). The event handler must look the item up in a server-side config, take the price from there, check the player can afford it, and refuse anything else. The fivem-security skill has the full checklist, but the short version is: the client asks, the server decides.

RegisterNetEvent('shop:buy', function(itemName)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    local item = Config.Items[itemName]
    if not Player or not item then return end
    if Player.PlayerData.money.cash < item.price then return end
    if Player.Functions.RemoveMoney('cash', item.price, 'shop-purchase') then
        Player.Functions.AddItem(itemName, 1)
    end
end)

6. Outdated database calls

Older tutorials used mysql-async and ghmattimysql. Modern servers run oxmysql. The model happily writes MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier = @id', {['@id'] = id}, function(result) ... end). oxmysql kept a compatibility layer for those names for a while, but the current, documented API is:

local rows = MySQL.query.await('SELECT * FROM users WHERE identifier = ?', { identifier })
local id = MySQL.insert.await('INSERT INTO vehicles (owner, plate) VALUES (?, ?)', { owner, plate })

Relying on the legacy names means your script breaks the day the compatibility layer goes, and callback pyramids are harder to review anyway. The oxmysql skill documents the await variants and the MySQL.prepare and MySQL.transaction calls the model rarely uses correctly.

Why prompting harder does not fix it

You can put “use QBCore, not ESX” in every prompt and the model will still slip, because the problem is what it learned, not what you asked. It has no reliable memory of which QBCore.Functions.* signature is current, which lib. functions exist, or what MySQL.query.await returns. Corrections in the prompt compete with thousands of outdated examples in training data.

The thing that works is putting the correct reference in front of the model at the moment it writes. That is what a skill is.

The fix: give the model real docs with skills

A skill is a folder with a SKILL.md file at the top and reference files next to it. SKILL.md describes what the skill covers and when to use it. The reference files hold the actual API: function signatures, event names, manifest snippets, common errors and their fixes.

Agents like Claude Code and Cursor read the SKILL.md descriptions at the start of a session, and when your request matches one, they load the full skill into context before writing code. So when you ask for a QBCore shop, the model reads Player.Functions.AddItem, Player.PlayerData.money, and the server-side validation pattern from the reference, not from memory.

Installing ours takes a minute:

  1. Open the FiveM skills page and download the ones for your stack. For most servers that is fivem-basics, oxlib, oxmysql and either esx-framework or qbcore-framework.
  2. Unzip each one into the skills folder your tool uses (.claude/skills/ for Claude Code, .cursor/rules/ for Cursor, .github/instructions/ for Copilot). The install notes on each skill page have the current paths.
  3. Start a new session and ask “what FiveM skills do you have loaded?” to confirm.

From then on, the model writes MySQL.query.await because that is what the reference says, and it puts money handling on the server because the security skill told it to.

Review checklist for AI-written FiveM resources

Even with skills loaded, review every resource before you ensure it. Ten checks, in order:

  1. Every native exists on docs.fivem.net.
  2. Only one framework is referenced, and it is yours.
  3. No database, money or inventory calls in client.lua.
  4. fxmanifest.lua has fx_version 'cerulean', game 'gta5' and lua54 'yes' when needed.
  5. @ox_lib/init.lua and @oxmysql/lib/MySQL.lua are declared if the code uses them.
  6. Every server event handler validates source and its arguments.
  7. Prices, rewards and item names come from server config, never from the event.
  8. Database calls use MySQL.*.await or callbacks with ? placeholders, never string concatenation.
  9. Citizen.Wait(0) loops only run while they need to (a while true do Wait(0) that draws a marker across the whole map is a frame-time bug waiting to happen).
  10. No TriggerClientEvent(-1, ...) for things only one player should see.

Ten minutes with this list catches most of what a cheat menu would otherwise find for you.

Frequently asked questions

Can ChatGPT write a working FiveM script at all?

Yes, for small standalone resources with clear instructions, and much more reliably when it has skills or documentation in context. It struggles with anything that depends on a specific framework version or a paid resource it has never seen.

Does this happen with Claude, Copilot and Gemini too?

Yes. All large language models share the same training data problem: outdated FiveM examples in many incompatible flavours. The models differ in how well they follow the docs you give them, which is why skills help across all of them.

Is a skill the same as a Cursor rule or a system prompt?

Close. A Cursor rule or a CLAUDE.md is a short instruction that is always on. A skill is loaded only when relevant and can carry many reference files, so it holds a full API reference without bloating every prompt. Our skills ship as SKILL.md plus references and can be dropped into either mechanism.

Add these skills to Cursor, VS Code or Claude Code so the AI knows the real APIs mentioned in this post.