How to make a FiveM script with AI (step by step)
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:
- A local test server. The docs walk through setting one up with txAdmin. Never test on a live server.
- A code editor, and whichever AI coding tool you already use, whether that's a chat window, an editor assistant or an agent in the terminal.
- Lua 5.4 and luacheck on your PC for checks. They're optional, but they catch a lot.
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:
fx_version 'cerulean'is the current version, per the resource manifest reference.game 'gta5'means FiveM. (rdr3is RedM, andcommonruns on either but can't use game-specific functions.)client_scriptandserver_scriptdecide where each file runs. If a server file ends up underclient_script, it ships to every player's PC, so read these two lines carefully.lua54 'yes'shows up in a lot of AI answers and older tutorials. The docs now mark it deprecated: since June 2025 all Lua scripts run on 5.4, so you don't need it. If your AI adds it, its training data is out of date, and that's worth keeping in mind for the rest of its answer.
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)
RegisterCommandregisters/team. The handler getssourceandargs, so/team bluegivesargs = { 'blue' }. Client code doesn't needsource, so it's named_.TriggerServerEventsends the request.RegisterNetEventis needed for any event that crosses the network in Lua (listening for events).chat:addMessagecomes from the defaultchatresource. It isn't a native, so it only works if that resource is running.
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:
- Who sent it? Use
source, which the server sets for you. Never take a player ID from the client. - Too often? Ignore requests inside a cooldown.
- Right type? The client can send anything, so check
type()first. - Allowed value? Compare it against a list the server owns.
- 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:
- Save
sourceto a local straight away. The docs say the globalsourceis only valid for the first call of the event. After aWait, it's reset.local src = sourceon the first line avoids a nasty bug later. GetGameTimeris available on the server and returns milliseconds, which is fine for a cooldown (native reference).TriggerClientEventneeds a target. Passsrcto answer one player, or-1to send to everyone. An AI that writes-1where it meantsrcwill spam the whole server.playerDroppedfires when a player leaves, withsourceset to that player (event reference). Without it, your tables keep growing for as long as the server runs./myteamis registered on the server, so it answers from the server's own records rather than anything the client claims.
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:
- Does it exist? Made-up natives with plausible names are a common AI mistake in FiveM code.
- Is it available on the side you're calling it from? Many natives are client-only. A client native called in
server.luajust errors. - Are the arguments right? Count them and check their order.
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:
- Spam
/team red. The cooldown should swallow the extra requests. - Send junk such as
/team admin,/teamwith nothing after it, or a very long word. - Join with a second player and check that each one has their own team and cooldown.
- Leave and rejoin, then run
/myteam. It should saynone.
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 did | Why it's a problem | What to ask for instead |
|---|---|---|
| Server event takes a player ID or amount from the client | Cheaters can call it with anything | "Use source, and decide amounts on the server" |
| Uses a native that doesn't exist | The script errors when it loads | "Only use natives from docs.fivem.net and list them" |
| Calls a client-only native in server code | Errors on the server | "Which side is each native available on?" |
Reads source after a Wait | Wrong or empty player | "Save source to a local first" |
| No cooldown on server events | One client can flood the server | "Add a per-player cooldown" |
| Never cleans up per-player tables | Memory grows forever | "Clear player data on playerDropped" |
Adds lua54 'yes' | Harmless, but a sign of old training data | Check the manifest reference |
| Mixes framework code (QBCore, Qbox, ESX) | Errors everywhere | Name one framework and its version in the prompt |
Where to go next
- The resource manifest reference and the events docs are worth reading in full once.
- When you move on to jobs, money and items, you'll add a framework. Pick one, read its docs and put its name and version in every prompt.
Next step
- The FiveM Server Pack (coming soon) is a set of prompts built around the same "the server decides" rules.
- New to FiveM servers? Our beginner guide isn't on the site yet. Until it is, start from the RedM and FiveM prompts hub.
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.