Get featured

How to make a FiveM script with AI (step by step)

Guide · checked against the docs on

AI coding tools are good at FiveM Lua. They know the shape of a resource, they can write event handlers in seconds, and they will happily explain an error message at 2 AM. They are also confident when they're wrong. They invent natives, mix up client and server code, and write server events that any cheater can call with whatever they like.

This guide walks through one small resource from start to finish, the way we'd do it with an AI tool open next to the editor: ask for one thing, read what comes back, check it against the docs, lint it, test it, then try to break it.

Not affiliated with or endorsed by Rockstar Games, Take-Two or Cfx.re. If this guide and the Cfx.re docs ever disagree, trust the docs.

What you'll build

A /team command. The player types /team blue, and the server checks the request and puts them on a team. A second command, /myteam, asks the server which team you're on.

It's deliberately boring. It's small enough to read in one sitting, and it has every part that goes wrong in real scripts: a client event, a server event, a value sent by the player, and state that has to be cleaned up when someone leaves.

You need:

Step 1: Tell the AI the rules before it writes anything

Most bad AI output comes from a vague prompt. Give it the platform, the goal and the security rules up front. Here's the prompt we'd start with:

I'm writing a standalone FiveM resource in Lua (no framework).
Goal: a /team command. The player picks red, blue or green.
Rules:
- The client only sends the request. The server decides.
- On the server, use `source` to know who sent an event. Never accept a player ID from the client.
- Check the type and value of every argument the client sends.
- Add a per-player cooldown on the server event.
- Clean up per-player data on playerDropped.
- Give me fxmanifest.lua, client.lua and server.lua.
- Only use functions from docs.fivem.net. List every native you used.

That last line matters. Asking the AI to list what it used gives you a short checklist to look up, and if it can't name where a function comes from, that's a red flag. (We haven't scored AI runs with this prompt. It's a starting point, not a tested recipe.)

Step 2: Check the manifest

Every resource needs an fxmanifest.lua. It tells the server what the resource is and which files run where. This is what a correct one looks like for our script:

fx_version 'cerulean'
game 'gta5'

name 'swai_team'
description 'Example: the player asks to join a team, the server checks and decides'
version '0.1.0'

client_script 'client.lua'
server_script 'server.lua'

What to check in whatever the AI gives you:

Step 3: The client only asks

client.lua runs on the player's PC. A player can change it or skip it entirely and fire events by hand. So the client does as little as possible:

-- swai_team/client.lua
-- Runs on the player's PC. It only asks. The server decides.

RegisterCommand('team', function(_, args)
    -- /team blue  ->  args = { 'blue' }
    TriggerServerEvent('swai_team:join', args[1])
end, false)

RegisterNetEvent('swai_team:reply', function(message)
    TriggerEvent('chat:addMessage', { args = { 'Team', message } })
end)

Step 4: The server decides

This is the part to read slowly. When you ask an AI for "a server event that sets the player's team", you'll often get something like this:

-- DON'T: trusts a player ID and a value sent by the client
local teamOf = {}

RegisterNetEvent('swai_team:join', function(playerId, team)
    teamOf[playerId] = team  -- any client can set anyone's team to anything
end)

return teamOf

It works in testing, and it's broken. Any client can call swai_team:join with any player ID and any value: another player's ID, a team that doesn't exist, a table, a huge string. The server just believes it. Swap "team" for "money" or "item" and you get the classic way FiveM servers get abused.

The fix is a short checklist the server runs every time:

  1. Who sent it? Use source, which the server sets for you. Never take a player ID from the client.
  2. Too often? Ignore requests inside a cooldown.
  3. Right type? The client can send anything, so check type() first.
  4. Allowed value? Compare it against a list the server owns.
  5. Only then change state, and only for that player.

Here's the server file that does that:

-- swai_team/server.lua
-- Runs on the server. Treat everything the client sends as untrusted.

local ALLOWED_TEAMS = { red = true, blue = true, green = true }
local COOLDOWN_MS = 3000

local teamOf = {}     -- [player server id] = team name
local lastAsk = {}    -- [player server id] = time of last request

RegisterNetEvent('swai_team:join', function(team)
    local src = source  -- who sent it; save it before anything else

    -- 1. Rate limit: ignore spam.
    local now = GetGameTimer()
    if lastAsk[src] and now - lastAsk[src] < COOLDOWN_MS then
        return
    end
    lastAsk[src] = now

    -- 2. Check the type. A cheater can send a table, a number or nothing.
    if type(team) ~= 'string' then
        TriggerClientEvent('swai_team:reply', src, 'Usage: /team red|blue|green')
        return
    end

    -- 3. Check the value against a fixed list the server owns.
    team = team:lower()
    if not ALLOWED_TEAMS[team] then
        TriggerClientEvent('swai_team:reply', src, 'Unknown team. Pick red, blue or green.')
        return
    end

    -- 4. Only now change state, and only for the player who asked.
    teamOf[src] = team
    print(('[swai_team] %s joined %s'):format(GetPlayerName(src) or src, team))
    TriggerClientEvent('swai_team:reply', src, 'You joined ' .. team .. '.')
end)

-- Server-side command: the server answers from its own records.
RegisterCommand('myteam', function(src)
    if src > 0 then
        TriggerClientEvent('swai_team:reply', src, 'Your team: ' .. (teamOf[src] or 'none'))
    end
end, false)

AddEventHandler('playerDropped', function()
    teamOf[source] = nil
    lastAsk[source] = nil
end)

A few details the AI often gets wrong:

Step 5: Check the AI's work against the docs

Before running anything, go through the list of natives the AI gave you and look up each one in the natives reference. You're checking three things:

If something doesn't check out, paste the doc page into the chat and ask the AI to fix that one function. That works much better than asking it to "fix the bugs".

Step 6: Lint it before you load it

Run luacheck on the folder. Tell it which FiveM globals exist so it doesn't flag every native, with a .luacheckrc like this:

std = 'lua54'
files['swai_team/fxmanifest.lua'] = { allow_defined_top = true, globals = {}, read_globals = { 'fx_version','game','name','description','version','client_script','server_script' } }
read_globals = { 'RegisterCommand','TriggerServerEvent','RegisterNetEvent','TriggerEvent','TriggerClientEvent','AddEventHandler','GetGameTimer','GetPlayerName','source' }

Then:

luacheck swai_team

luacheck catches typos in global names (TriggerSeverEvent shows up as an unknown global), unused variables and shadowed locals. It caught a real issue while we were writing this guide: our first draft stored teamOf but never read it, and that's what led to adding /myteam.

A clean lint run means the syntax is fine. It doesn't prove the logic is right.

Step 7: Test it, then try to break it

Load it. Put the folder in resources/[local]/swai_team, then in the server console run refresh to rescan resources and ensure swai_team to start or restart it (server commands). Add ensure swai_team to server.cfg so it starts with the server. Watch the console for red errors.

Use it. Join your local server and try /team blue, then /myteam. Client errors show up in the F8 console.

Break it. This is the step most people skip:

You can also ask your AI tool to write an offline test with fake versions of the FiveM functions. We did that for this guide: a plain Lua file that stubs RegisterNetEvent, TriggerClientEvent and the rest, then calls the server handler directly with good and bad input (a valid team, spam, a table, an unknown name, nothing, a second player, and a disconnect). It isn't a real server, but it catches logic bugs in seconds, before you even start the game.

Common AI mistakes in FiveM scripts

What the AI didWhy it's a problemWhat to ask for instead
Server event takes a player ID or amount from the clientCheaters can call it with anything"Use source, and decide amounts on the server"
Uses a native that doesn't existThe script errors when it loads"Only use natives from docs.fivem.net and list them"
Calls a client-only native in server codeErrors on the server"Which side is each native available on?"
Reads source after a WaitWrong or empty player"Save source to a local first"
No cooldown on server eventsOne client can flood the server"Add a per-player cooldown"
Never cleans up per-player tablesMemory grows forever"Clear player data on playerDropped"
Adds lua54 'yes'Harmless, but a sign of old training dataCheck the manifest reference
Mixes framework code (QBCore, Qbox, ESX)Errors everywhereName one framework and its version in the prompt

Where to go next

Next step

Not affiliated with or endorsed by Rockstar Games, Take-Two, Cfx.re or any framework project. Names are used only to say what this guide is about.