START HERE·BUILD REAL SCRIPTS·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this whole lesson - every step, code block, and the exact console errors - plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.
Start Here · Build real scripts

Build a teleport command from scratch

In the last lesson you built a command from an empty folder and watched it fire. This time the command does something you can see in the world: type /tp spawn and your character jumps to a saved spot. Along the way you will meet the one rule every real server lives by, the server decides and the client moves, and you will write a per-player cooldown so nobody can spam it. By the end you type /tp garage, your character is at the garage, and a second /tp too soon is refused.

You'll build
One complete resource named qu_teleport with a /tp <name> command that moves your character to a saved, named location.
Time
~15 minutes
Difficulty
Beginner
You need
Your local test server, a text editor (VS Code is free), and the command you built in the last lesson under your belt.

Watch a similar script

Related, not the exact code: a teleport script in action. This is a different script than the qu_teleport you build here, shown so you can see what a teleport script does. The real output of your build is in the steps below.
BEFORE YOU START

Why it exists

A teleport command is the first thing almost every server builder writes, because it is the fastest way to get somewhere without driving. Admins use it to reach a problem player. Builders use it to test a custom interior without spawning a car. It is also the cleanest possible example of a script that has to reach across the network: the typed command lands on one machine, but the thing it changes (your character's position) lives on another. Learning where each half runs here is the whole point, because every command that grants something real (a heal, an item, a repair) has exactly this shape.

Vocabulary

location
A named spot in the world, stored as three numbers: x, y, and z coordinates. We keep a small list of them in a config file so a player types a name, not raw numbers.
cooldown
A minimum wait before an action is allowed again. Here it stops a player from teleporting over and over. We store the moment of the last teleport and compare it to now.
server authority
The rule that the server, not the player's PC, makes every decision that matters. The player can edit their own machine, so the server must own the rulebook.

The plan

You will build a resource named qu_teleport. The qu_ prefix is the practice naming habit from earlier lessons, so your own scripts never clash with a downloaded one. It has four files: a manifest that lists them, a config file holding your named locations and the cooldown length, a server file that owns the decision, and a client file that moves the character.

Here is the notional machine, the picture of what the running code actually does. The server reads all the files top to bottom when the resource starts, and it builds one private list: a cooldown table that starts empty. When a player types /tp garage, that request lands on the server. The server looks up garage in the config, checks the player is not on cooldown, stamps the time, and sends a single message to that one player saying "move to these coordinates." The client receives that message and does the one thing only it can do: it sets the character's position. The server never moves the character itself, because the character only exists on the player's PC. The client never owns the cooldown, because a player can edit anything on their own machine. Success looks like your character standing at the saved spot, with the move logged in two consoles.

What happens behind the scenes

Here is the folder you are about to create and the path one /tp garage takes through it.

text
resources/qu_teleport/
  fxmanifest.lua    lists the files (loads first)
  config.lua        named locations + cooldown length (shared)
  server.lua        looks up the name, checks cooldown, decides (server side)
  client.lua        moves the character (client side)
text
player types /tp garage (in chat)
        |
        v
SERVER  is "garage" a known location?
        |
        +-- no   -->  print "unknown location", stop
        |
        +-- yes  -->  on cooldown?
                          |
                          +-- yes  -->  print "wait Ns", stop
                          |
                          +-- no   -->  stamp os.time() in the table
                                        send coords to that one player
                                                |
                                                v
                        CLIENT  receives coords  -->  SetEntityCoords(ped, x, y, z, ...)  -->  moved
SERVER
the host
network
CLIENT
the player's game

The shape to memorize: the server holds the rulebook (the location list and the cooldown) and makes the decision, the client only applies the move it was told to apply.

Build it

Follow the folder names, file names, and code exactly. Each step has you predict the result before you see it. Once it all works, the next section takes it apart line by line.

Make the qu_teleport folder

The server has one folder for this resource.

Inside your server's resources folder, create this folder:

text
resources/qu_teleport

Then create four empty files inside it so the layout looks exactly like this:

text
resources/qu_teleport/
fxmanifest.lua
config.lua
server.lua
client.lua

Write fxmanifest.lua

FiveM knows which files to load and where each runs.

The manifest is the file FiveM reads first to learn which other files belong to this resource and which side each one runs on. Open fxmanifest.lua and paste this:

lua
fx_version 'cerulean'
game 'gta5'
 
shared_script 'config.lua'
server_script 'server.lua'
client_script 'client.lua'

server_script runs on the server machine. client_script runs on every player's PC. shared_script is loaded on both sides, which is exactly what config.lua needs to be, because the server reads the location list to decide and (in your own later versions) the client may read it too. Do not add a lua54 'yes' line: as of June 2025 it is deprecated and ignored, because Lua 5.4 is now the only runtime.

Write config.lua

Your named locations and cooldown live in one place.

Predict before you write it: this file does not teleport anyone or print anything. So what do you expect to see in the console when the resource restarts after you add it? (Answer: nothing. It only defines values for the other files to read.)

Open config.lua and paste this:

lua
Config = {}
 
Config.Cooldown = 10
 
Config.Locations = {
spawn = { x = -1037.7, y = -2738.0, z = 20.2 },
garage = { x = -337.2, y = -136.8, z = 39.0 },
pier = { x = -1850.1, y = -1231.7, z = 13.0 },
}

Write server.lua

The server looks up the name, enforces the cooldown, and decides.

Predict before you write it: when the resource restarts, does this file teleport you or print anything yet? (Answer: no. It only registers the command and creates an empty cooldown table. Nothing fires until you type /tp.)

Open server.lua and paste this:

lua
local cooldown = {}
 
RegisterCommand('tp', function(src, args)
local name = args[1]
local place = Config.Locations[name]
 
if not place then
    print('[qu_teleport] unknown location: ' .. tostring(name))
    return
end
 
local now = os.time()
if cooldown[src] and now - cooldown[src] < Config.Cooldown then
    local wait = Config.Cooldown - (now - cooldown[src])
    print('[qu_teleport] denied, ' .. src .. ' must wait ' .. wait .. 's')
    return
end
 
cooldown[src] = now
TriggerClientEvent('qu_teleport:go', src, place.x, place.y, place.z)
print('[qu_teleport] sent ' .. GetPlayerName(src) .. ' to ' .. name)
end, true)

The cooldown table sits at the top of the file, outside the command, on purpose. We will explain why in the next section. The true at the end is the restricted flag you met last lesson.

Write client.lua

The client moves the character when it is told to.

Predict before you write it: this file does not run on restart either. What single thing do you expect it to do, and only when the server tells it? (Answer: set your character's position to the coordinates the server sent.)

Open client.lua and paste this:

lua
RegisterNetEvent('qu_teleport:go', function(x, y, z)
SetEntityCoords(PlayerPedId(), x, y, z, false, false, false, true)
print('[qu_teleport] moved to ' .. x .. ', ' .. y .. ', ' .. z)
end)

RegisterNetEvent is required here because this event arrives from across the network. Without it the message is rejected and nothing moves.

Ensure it and test it

Your character jumps to the saved spot, and the cooldown refuses a fast retry.

Open server.cfg and add this line:

text
ensure qu_teleport

Save it. Now open the txAdmin Live Console (the box at the bottom of the txAdmin web panel where you type server commands) and run this to start the resource right now:

text
ensure qu_teleport

Use ensure, not restart, the first time. The server.cfg line is only read when the whole server boots, so on an already-running server the resource has not started yet. ensure starts it if it is stopped and restarts it if it is already running, so it always works.

Now join your own server, wait until your character has fully spawned in, open the in-game chat box (press the chat key, by default T), and run the command with a location name:

text
/tp garage

The server line lands in the txAdmin Live Console, and the client line lands in your F8 console (press F8 in game). Your character is now at the garage.

Now run /tp pier again immediately, before the cooldown passes. The server refuses and no move is sent:

Finally, type a name that is not in the config to see the rejection path:

text
/tp moon

How it works

You ran it and your character moved. Now take the code apart so next time you are deciding, not copying. There are four ideas here, one per piece, and each one lands on a specific side of the network.

The config table: names instead of numbers

lua
Config = {}
 
Config.Cooldown = 10
 
Config.Locations = {
    spawn = { x = -1037.7, y = -2738.0, z = 20.2 },
    garage = { x = -337.2, y = -136.8, z = 39.0 },
    pier = { x = -1850.1, y = -1231.7, z = 13.0 },
}

This file runs on both sides (it is a shared_script), but for now only the server reads it. Config is a Lua table, the same kind of lookup list you used for the cooldown in the heal capstone. The difference is what it stores. Config.Locations is a table whose keys are names (spawn, garage, pier) and whose values are themselves tiny tables of three numbers: x, y, and z. Those three numbers are a point in the GTA world, the same coordinates the map editor shows you. Pulling these out into a config file is a habit, not a requirement: it means a player types a friendly word and you change a spot, or add a new one, without ever touching the logic in server.lua. Config.Cooldown = 10 is the wait in seconds, named once so you can tune it in one place.

Reading the typed name and rejecting unknowns

lua
RegisterCommand('tp', function(src, args)
    local name = args[1]
    local place = Config.Locations[name]
 
    if not place then
        print('[qu_teleport] unknown location: ' .. tostring(name))
        return
    end

This runs on the server. RegisterCommand is the FiveM native you met last lesson: it wires the typed word tp to this block of code and runs the block later, when someone types /tp. FiveM hands the block two values you use here. src is the source, the server id of the player who ran it (the console is source 0). args is a table of the words typed after the command, so for /tp garage the table holds garage at position 1. args[1] reads that first word, and we name it name.

Config.Locations[name] is a table lookup: it asks the config "do you have an entry under this key?" If the player typed garage, it hands back that location's { x, y, z } table. If they typed something not in the list, like moon, there is no entry, so the lookup hands back nil, Lua's word for "nothing here." The if not place then line reads in plain English as "if we found nothing," and on a miss it prints a message and returns, which stops the function right there so none of the teleport code below ever runs. tostring(name) guards the print: if the player typed /tp with no name at all, name is nil, and tostring turns that nil into the readable text nil instead of crashing the join. Rejecting bad input first, then returning, is the cleanest way to handle a decision: deal with the wrong case, leave, and let the good case be the rest of the function.

The per-player cooldown

lua
    local now = os.time()
    if cooldown[src] and now - cooldown[src] < Config.Cooldown then
        local wait = Config.Cooldown - (now - cooldown[src])
        print('[qu_teleport] denied, ' .. src .. ' must wait ' .. wait .. 's')
        return
    end
 
    cooldown[src] = now

This runs on the server too. os.time() is a built-in Lua function that returns the current time as a whole number of seconds, and subtracting two readings tells you how many seconds passed between them. cooldown is the table at the top of the file: its key is a player's src, its value is the moment that player last teleported. cooldown[src] is that last reading, or nil if this player has never teleported. The if reads as "if this player has teleported before, and fewer than Config.Cooldown seconds have passed since then, refuse." The cooldown[src] and part guards the first-ever teleport: if the value is nil, the whole condition is false and the move is allowed. On a refusal we print how many seconds are left and return. If the code gets past the if, the request passed, so cooldown[src] = now stamps this moment, and the next attempt is measured from here.

Why does the table live at the top of server.lua, outside the command? Because it has to survive between calls. A local declared inside the handler would be rebuilt empty every single time the command fired, so it would forget the last teleport and never block anything. Declared once at the top, it persists for the life of the resource and remembers every player.

Sending the move and applying it on the client

lua
    TriggerClientEvent('qu_teleport:go', src, place.x, place.y, place.z)
    print('[qu_teleport] sent ' .. GetPlayerName(src) .. ' to ' .. name)
end, true)

The first line runs on the server. TriggerClientEvent is a FiveM native the server uses to send a message to one specific player. The first argument is the event name, qu_teleport:go. The second is the target, src, the id of the one player who asked, so the message travels only to them and not to everyone on the server. After the target you can pass extra values along, and here we pass the three coordinates the server looked up. GetPlayerName(src) returns that player's name for the log line. The trailing true is the restricted flag: only the server console and identities you grant an ace permission may run this command. Set it to false and any connected player could fire it from chat, which is exactly how servers get exploited, so true is the safe default for anything that changes game state.

lua
RegisterNetEvent('qu_teleport:go', function(x, y, z)
    SetEntityCoords(PlayerPedId(), x, y, z, false, false, false, true)
    print('[qu_teleport] moved to ' .. x .. ', ' .. y .. ', ' .. z)
end)

This runs on the client, on the player's own PC. RegisterNetEvent listens for the qu_teleport:go message; it is required for any event that crosses the network, or the message is silently rejected. The three coordinates the server sent arrive as the handler's arguments x, y, z. PlayerPedId() is a client native that returns the handle of your character, the body the camera follows. SetEntityCoords is the client native that actually moves an entity, and its arguments are, in order: the entity (your ped), the three coordinates, and then three booleans you leave false (they control extra movement options you do not need), and a final true that clears the area around the landing spot so you do not appear stuck inside another object. Both PlayerPedId and SetEntityCoords are client natives: they only exist on the client, because the character lives on the player's machine. This is server-decides, client-acts made real. The server said where; the client did the moving.

Why does the server send coordinates to the client instead of just moving the character itself?

Because the character (the ped) is a client-side entity: it only exists on the player's own PC, so the natives that move it, PlayerPedId and SetEntityCoords, only exist on the client. The server has no ped to move. So the server does the part it can do, deciding whether the teleport is allowed and which coordinates to use, then sends those coordinates to the one player with TriggerClientEvent. The client receives them and runs SetEntityCoords. The general rule: every decision lives on the server, and only the visible effect lives on the client.

Common mistakes

SymptomFix
attempt to call a nil value (global 'SetEntityCoords')You put SetEntityCoords (or PlayerPedId) in server.lua. They are client natives and only exist on the client. Move them into client.lua, inside the RegisterNetEvent handler, and let the server only send the coordinates.
/tp garage prints 'unknown location: garage' even though garage is in the configThe name does not match the config key exactly. Lua keys are case sensitive, so Garage and garage are different, and a typo or trailing space breaks the lookup. Compare the typed word to the key in Config.Locations character for character.
The cooldown never blocks: you can /tp again instantly every timeThe cooldown table was declared inside the command handler (local cooldown = {} below RegisterCommand), so it is rebuilt empty on every call and forgets the last teleport. Move local cooldown = {} to the top of server.lua, outside the handler, so it persists between calls.
'end' expected (near &lt;eof&gt;)The RegisterCommand function has no closing end, or an if block is missing its end. Every function and every if must finish with its own end. Pair each one by eye: the command needs end, true) at the very bottom.
The server prints 'sent ... to garage' but the character never moves and F8 shows nothingThe client is missing RegisterNetEvent for qu_teleport:go, or the event name does not match on both sides. A networked event is rejected unless its exact name was registered with RegisterNetEvent on the receiving side. Compare the two strings character for character.

Recap

  • Build a four-file resource: an fxmanifest.lua that lists the files, a shared config, a server file, and a client file.
  • Keep named locations and a cooldown length in config.lua so you change a spot or a number in one place, not in the logic.
  • Read the typed location name from args[1] on the server, look it up in a table, and reject an unknown name with an early return.
  • Enforce a per-player cooldown with a server-side table and os.time, declared at the top of the file so it persists between calls.
  • Send coordinates to one player with TriggerClientEvent and apply the move on the client with PlayerPedId and SetEntityCoords, because the ped is client side.

Try it yourself

You just built a real teleport that respects server authority and a cooldown, the same spine behind heal, give, and repair commands. Next you take this further into something with state that has to be stored and trusted: the next lesson, "Build a banking script," has the server hold a balance per player, so a deposit on the client can never lie about how much money you have.