AI Core

Talking to the server

Ask the server and wait for its answer, run server actions with translated refusals, listen to server events and answer server callbacks.

A client script never trusts itself: it asks the server, and the server decides. These helpers carry those questions and answers. The server halves are on Client input (onClientCallback, onClientEvent, triggerClientCallback).

You want toUse
Run something the server must allow (buy, rent, open) and show its refusalcallAction
Ask the server for datatriggerServerCallback
Hear something the server sendsonServerEvent
Answer a question the server asks this clientonServerCallback

triggerServerCallback

Asks the server and waits for its answer. The server answers with onClientCallback(name, ...). Use it to read data the server keeps (a price list, a player's own records). For an action that can be refused, use callAction: it shows the refusal for you.

Side: Client · Waits: yes (Promise in TypeScript, yields the thread in Lua)

Signature
triggerServerCallback(name: string, args?: unknown[], timeoutMs?: number): Promise<unknown>
Lua
local result, err = aiLib.triggerServerCallback(name, args, timeout) -- call inside a thread

Parameters

Prop

Type

Returns

TypeScript: Promise<unknown>. It resolves with what the server handler returned (null when it returned nothing). The value is not checked: test its type before you use it. The promise rejects when the server handler threw, the server gate refused the call (rate limit, a guard, bad arguments, a lock), or no answer came within timeoutMs.

Lua: two values. result is the answer; err is nil on success, 'failed' when the server refused or failed, 'timeout' when no answer came in time. On an error result is nil.

Example

resource/client/index.ts
import { createLocale, notify, triggerServerCallback } from "@aicore-fivem/lib/client";

const Locale = createLocale<typeof import("../../locales/en.json")>();

RegisterCommand(
  "mycash",
  async () => {
    const cash = await triggerServerCallback("myscript:cash").catch(() => null);
    if (typeof cash !== "number") return notify(Locale("ERROR"), "error");
    notify(Locale("YOUR_CASH", cash), "info");
  },
  false,
);
resource/server/index.ts
import { onClientCallback, resourceExport } from "@aicore-fivem/lib/server";

const core = resourceExport("ai-core");

onClientCallback("myscript:cash", (source) => core.GetMoney(source, "cash"));

In TypeScript always handle the rejection (.catch(() => null) or try): a refused or timed out call rejects. The server limits each callback to 20 calls per second per player by default, so never call it in a loop or every frame. The wire format is the same in both languages: a Lua client can call a TypeScript server and the other way round.

callAction

Runs an action on the server and shows the refusal for you. The server handler (onClientCallback) answers an ActionResult: { ok: true, ...data } when it worked, or fail("LANG_KEY") when it refused. On a refusal, a timeout or a gate refusal, callAction looks the error key up in your resource's lang file and shows it as an error notification. Use it for every button that changes something: buy, sell, rent, repair.

Side: Client · Waits: yes (Promise in TypeScript, yields the thread in Lua)

Signature
callAction(name: string, args: unknown[] | undefined, options: CallActionOptions): Promise<({ ok: true } & Record<string, unknown>) | null>
Lua
local reply = aiLib.callAction(name, args, options) -- table or nil, call inside a thread

Parameters

Prop

Type

CallActionOptions

Prop

Type

Returns

Promise<({ ok: true } & Record<string, unknown>) | null>: the whole ok reply (with any extra fields the server added) when the action worked; null when it was refused, failed or timed out (the notification is already shown unless silent). The extra fields are not checked: test the type of each one you read. Lua returns the reply table or nil.

Example

resource/client/index.ts
import { callAction, createLocale, notify } from "@aicore-fivem/lib/client";

const Locale = createLocale<typeof import("../../locales/en.json")>();

export async function buy(itemId: string): Promise<void> {
  const reply = await callAction("myshop:buy", [itemId], { fallback: "BUY_FAILED" });
  if (!reply) return; // the refusal is already on screen
  const paid = typeof reply.paid === "number" ? reply.paid : 0;
  notify(Locale("BOUGHT", paid), "success");
}
resource/server/index.ts
import { inventory, onClientCallback, resourceExport } from "@aicore-fivem/lib/server";
import { fail, oneArg, readText } from "@aicore-fivem/lib/shared";

const core = resourceExport("ai-core");
const PRICES: Record<string, number> = { water: 5, bread: 8 };

onClientCallback(
  "myshop:buy",
  { args: oneArg((v) => readText(v, 32)) },
  (source, item) => {
    const price = PRICES[item];
    if (price === undefined) return fail("UNKNOWN_ITEM");
    if (!inventory.canCarryItem(source, item, 1)) return fail("NO_ROOM");
    if (!core.RemoveMoney(source, "cash", price, "shop")) return fail("NO_MONEY");
    inventory.addItem(source, item, 1);
    return { ok: true, paid: price };
  },
);

The error key is looked up in the lang file of the resource that calls callAction, so put every key the server can answer (NO_MONEY, NO_ROOM, UNKNOWN_ITEM) and the fallback key in that resource's locales/en.json. A key missing there is shown as the key itself.

isActionOk

Tells whether a reply is an ok ActionResult (ok is exactly true). Use it when you call triggerServerCallback yourself and read the reply by hand.

Side: Client and server (from /shared, re-exported by /client) · Waits: no

Signature
isActionOk(reply: unknown): reply is { ok: true } & Record<string, unknown>

No Lua twin: test type(reply) == 'table' and reply.ok == true.

Parameters

Prop

Type

Returns

boolean: true when reply is an object (not an array) whose ok is true. In TypeScript it also narrows reply to { ok: true } & Record<string, unknown>.

Example

import { isActionOk, replyError, triggerServerCallback } from "@aicore-fivem/lib/client";

export async function buyWater(): Promise<void> {
  const reply = await triggerServerCallback("myshop:buy", ["water"]).catch(() => null);
  if (isActionOk(reply)) console.log("bought");
  else console.log("refused with", replyError(reply, "BUY_FAILED"));
}

replyError

The lang key of a refused reply: its error field, or fallback when the reply is missing or has no error text. Use it with isActionOk when you show the error your own way (in a NUI page instead of a notification).

Side: Client and server (from /shared, re-exported by /client) · Waits: no

Signature
replyError(reply: unknown, fallback: string): string | null

No Lua twin.

Parameters

Prop

Type

Returns

string | null: null when the reply is ok; the reply's error when it is a non-empty string; otherwise fallback.

Example

import { replyError, triggerServerCallback } from "@aicore-fivem/lib/client";

export async function buyFromPage(item: string): Promise<void> {
  const reply = await triggerServerCallback("myshop:buy", [item]).catch(() => null);
  const error = replyError(reply, "BUY_FAILED");
  if (error) SendNUIMessage({ action: "error", data: error });
}

onServerEvent

Listens to a net event the server sends with emitNet (TriggerClientEvent in Lua). Calls that did not come from the server (another resource's local emit) are dropped, so a cheat menu cannot fake them. Every ai-* client listens to the server this way, never with a bare onNet. In TypeScript a handler that takes arguments must name an args parser: it checks the raw values, and null drops the call.

Side: Client · Waits: no

Signature
onServerEvent(name: string, handler: () => unknown): void
onServerEvent<A extends readonly unknown[]>(name: string, options: { args: (raw: unknown[]) => A | null }, handler: (...args: A) => unknown): void
Lua
aiLib.onServerEvent(name, handler) -- handler(...) gets the raw arguments

Parameters

Prop

Type

Returns

void.

Example

resource/client/index.ts
import { onServerEvent, Points } from "@aicore-fivem/lib/client";

let renting = false;
const points = new Points();

// No arguments: the short form.
onServerEvent("myscript:reset", () => points.clear());

// One boolean argument: parse it, null drops a bad call.
onServerEvent(
  "myscript:state",
  { args: ([active]) => (typeof active === "boolean" ? [active] : null) },
  (active) => {
    renting = active;
    points.refresh();
  },
);

The server sends the event with emitNet("myscript:state", source, true) (TypeScript) or TriggerClientEvent('myscript:state', source, true) (Lua). Lua handlers get the raw arguments: check their types yourself.

fromServer

Whether the net event being handled right now came from the server. onServerEvent already uses it; you need it only in a bare onNet handler you cannot replace.

Side: Client · Waits: no

Signature
fromServer(): boolean

No Lua twin: in a Lua net event handler, source is empty for local emits.

Returns

boolean: true when the current event carries the server's numeric source; false for a local emit by any resource.

Example

import { fromServer } from "@aicore-fivem/lib/client";

onNet("legacy:event", (value: unknown) => {
  if (!fromServer()) return; // a local emit, not the server
  console.log("server said", value);
});

Read it before the first await of the handler: after an await the current event is gone and the answer is wrong.

onServerCallback

Answers a question the server asks this client with triggerClientCallback(name, source, ...). The value your handler returns (or its promise resolves to) is sent back. A thrown error answers a failure, and the server's promise rejects.

Side: Client · Waits: no (the handler may be async)

Signature
onServerCallback(name: string, handler: (...args: unknown[]) => unknown): void
Lua
aiLib.onServerCallback(name, handler) -- the handler's return value is the answer

Parameters

Prop

Type

Returns

void.

Example

resource/client/index.ts
import { onServerCallback, player } from "@aicore-fivem/lib/client";

// The server asks how far this player stands from a point.
onServerCallback("myscript:distance", (x, y, z) => {
  if (typeof x !== "number" || typeof y !== "number" || typeof z !== "number") return null;
  return player.distanceTo({ x, y, z });
});

In Lua a handler that returns false answers nil (the server gets null): return true / nil, a number or a table when the server must tell false from "no answer". The answer comes from the player's computer, so a cheater can change it. Use client answers for things only the client knows (what is on screen, a local entity), and never as proof for money or items.

On this page