FiveM Exports, Events and Triggers: How Resources Talk to Each Other Without Breaking
Every server that runs more than a handful of resources depends on inter-resource communication. An inventory system needs to know when a job script wants to add an item. A phone needs to pull player data from the character system. A garage needs to lock vehicles when a player dies. Without a clean communication layer, you end up with scripts doing their own thing in isolation or, worse, copy-pasting functions between resources and maintaining them in two places. FiveM gives you two distinct mechanisms for this: exports and events. They solve different problems, and mixing them up is one of the most common architectural mistakes on new servers.
Exports: Synchronous Function Calls Across Resources
Exports are the right tool when resource A needs a return value from resource B right now. You define an export in the providing resource and call it from the consuming resource — no event registration, no callback, just a function call that blocks until it returns.
To expose a function, register it in the server script (or client script) of the providing resource:
exports('getPlayerJob', function(src)
return PlayerData[src] and PlayerData[src].job or nil
end)
Any other resource calls it as:
local job = exports['your-resource']:getPlayerJob(src)
The export call is synchronous — it returns a value inline, just like a local function call. If the providing resource isn’t running, the call throws a Lua error rather than silently returning nil. That’s a feature: it makes missing dependencies visible immediately instead of producing mysterious nil-related bugs downstream.
Exports vs Framework Shared Objects
QBCore and ESX both expose themselves via exports — exports[‘qb-core’]:GetCoreObject() and exports[‘es_extended’]:getSharedObject() are the pattern you’ll see everywhere. These return a table of functions and data that you cache in a local variable at resource start. Calling the export on every use adds unnecessary overhead; caching it once is the correct pattern. The frameworks are stable at startup, so the cached reference stays valid for the server’s lifetime.
Events: Asynchronous Notifications Across Boundaries
Events are the right tool when you need to notify something that you don’t need a return value from, or when you need to cross the client/server boundary. FiveM has three event directions:
- TriggerEvent(name, …) — local fire, same side (server-to-server or client-to-client). Runs synchronously within the current runtime.
- TriggerServerEvent(name, …) — client fires up to the server. Always async, always goes through the network layer even on localhost.
- TriggerClientEvent(name, source, …) — server fires down to a specific client (pass -1 to broadcast to all connected players).
Registration on the receiving end: RegisterNetEvent(‘yourresource:server:doThing’) followed by AddEventHandler(‘yourresource:server:doThing’, function(…) end). Skipping the RegisterNetEvent call means the server will ignore the event entirely — no error, just silence.
Recommended FiveM scripts for your server
Naming Convention and Why It Matters
Event names are global strings. If two resources both register ‘onPlayerLoaded’ as a net event, both handlers fire when either triggers it — and you won’t know which fired first. The CFX-recommended convention is resourceName:side:actionName, for example myjob:server:playerClocked. This namespaces events by origin and direction, making the firing resource identifiable from the handler and avoiding silent collisions. Enforce this across your server’s custom resources — a single team convention prevents a class of bugs that’s genuinely difficult to trace.
The Security Problem With TriggerServerEvent
Any client can call any registered net event. If your server-side handler for ‘bank:server:withdraw’ trusts the amount parameter without validating it, any player with a menu tool sends TriggerServerEvent(‘bank:server:withdraw’, math.huge) and drains the economy. Every server-side handler that receives data from a client must validate that data independently, regardless of what client-side logic you think guards it. Check types, clamp numbers, verify the player actually has permission for the action. The client is untrusted; the server event is a public API endpoint.
TriggerLatentServerEvent for Large Payloads
The standard TriggerServerEvent routes through the same network channel as position sync and other high-frequency updates. Sending more than a few kilobytes at once can noticeably stall that channel for the duration of the send. For large payloads — a full inventory snapshot, a serialized building config, a skin JSON blob — use TriggerLatentServerEvent(name, bps, …) and TriggerLatentClientEvent(name, source, bps, …) instead. The bps parameter caps the transfer rate; 131072 (128 KB/s) is a reasonable default that avoids channel saturation while still completing reasonably fast. The callback fires multiple times as chunks arrive; the final call has moreDataFollowing = false.
Upgrade your server — shop our FiveM scripts
Choosing Between Exports and Events
The decision rule is simple: need a return value synchronously? Use an export. Need to notify across the client/server boundary, or broadcast to multiple listeners? Use an event. The common mistake is using events for everything because they’re the more familiar pattern — firing an event to get a value back, then trying to read the result in a callback, and ending up with race conditions and nil checks everywhere. Exports exist precisely to eliminate that pattern.
One edge case: you can’t use exports to call from server to client or client to server. Exports are always within the same runtime environment. Cross-boundary synchronous communication doesn’t exist in FiveM’s architecture — if you need a value from the other side, you either structure the data so it’s already on the requesting side (state bags, cached player data) or you accept the asynchronous round-trip. This is a design constraint, not a bug, and designing around it produces cleaner architecture.
For framework-level communication patterns in practice, the resources at qb-tebex.io demonstrate how production QBCore scripts wire exports and events together without creating circular dependencies. If you’re running a mixed ESX/QBCore stack or auditing third-party scripts for event hygiene, 0resmon-tebex.io lists resources that document their inter-resource API surface explicitly. And for scripts where the communication layer has already been reviewed for security vulnerabilities before purchase, assets-tebex.io is worth checking — net event validation is one of the first things to audit in any new resource.