How to update a script
Take an ai-* script, a package or a third-party script to its new version and keep your settings, panel data, secrets, texts and database.
An update has two halves: install the new version exactly as it ships, then carry your server's own changes onto it. Copying your old files over the new ones is never the way: it brings back old code, old keys and old bugs, and the new version stops working in ways that are hard to find.
This page is written for two readers: you, and the AI coding agent (Claude Code, Codex, Cursor...) you point at it. Both follow the same procedure.
Ask your AI
Open your server data folder in your coding agent and paste this, with the scope filled in:
Update scripts on this AI Core server, following https://aifivem.com/docs/update-a-script.md
If this folder has .claude/skills/aicore-script-update/, read its SKILL.md and references: same procedure.
Scope: <every outdated ai-* script and package | ai-inventory ai-garages | the third-party scripts in C:\path\to\new-versions>
- First show me the list (name, path, current -> new version, release notes that need action) and wait for my go.
- Back up every folder before you touch it, git-ignored files such as data/secrets.json included.
- Get three trees per script: old upstream (the version I run, as shipped), ours (the folder now), new upstream.
Merge file by file with that three-way model. Never copy an old file over a new one.
- Keep my data/*.json panel content, data/secrets.json, my locale texts and my database.
Never print a secret in chat and never commit one.
- No SQL without my go and a backup of the affected tables. Never loosen security.cfg.
- Take generated files (dist/, fxmanifest.lua, resource/server/tables.ts, data/schemas/) from the new version
and rebuild them; never edit them by hand.
- Build, refresh, restart, and read the console log for errors, not only "Started resource".
- Do not commit or push unless I ask.
- End with a report per script, written in my language: version change, what upstream changed, what you carried
over and where, what was dropped (with the old value), new options left at their default, database, cfg changes,
build / restart / log result, what to test in game, and the decisions I must take.Every AI Core server folder has this procedure as a project skill in .claude/skills/aicore-script-update/: the txAdmin recipe installs it together with the rules, tools/, docs/compat/, CLAUDE.md and AGENTS.md, just like the hand install. Claude Code loads it by itself when you say "update my scripts" or "a new version is out"; other agents find it through AGENTS.md. A server installed with an older recipe may not have these files: download them from the aicore-server repository into your server folder as in Install by hand, keeping your own cfg files.
What stays yours
| What | Where | What the update does with it |
|---|---|---|
| Panel settings and content | data/*.json in each script | Kept. Converted to the new format only when the new version reads different fields. |
| Secrets | data/secrets.json (git-ignored) | Restored from the backup untouched. Never printed, never committed. |
| Your texts | locales/*.json, a third-party script's locales/ or languages/ | Your wording stays for every key that still exists; new keys are added in your language; removed keys go. |
| Your code changes | Any source file you edited | Written into the new version's code. A change with no place left is reported. |
| The database | Your tables and rows | Never dropped. New tables and columns are added at start; anything else waits for your go. |
| Security settings | security.cfg | Never loosened. If a release or a vendor asks for it, the update stops and you decide. |
Where the new version comes from
| Script | New version | You provide |
|---|---|---|
ai-* resources in resources/[ai]/ (ai-core, ai-lib, ai-mysql included) | The GitHub release of the repository named in its package.json ("repository": "github:aifivem/<name>") | Nothing |
The @aicore-fivem/* packages in packages/ | npm (npm view @aicore-fivem/<pkg> version) | Nothing |
Any other resource (usually in resources/[standalone]/) | The vendor | The new folder or zip |
ai-core checks every started script that has a GitHub repository (once every 6 hours per script) and prints <name> <current> is outdated: <latest> is out (<url>) in the console; the same list is on the admin dashboard. Update checks and the scripts they skip are set in the admin panel under General (updates.check, updates.ignore in ai-core's data/settings.json).
The three-way carry-over
Every update compares three versions of each file:
- Old upstream: the version your server runs, as it shipped.
- Ours: the folder as it is now, uncommitted changes included.
- New upstream: the new version, as it ships.
| Old -> ours | Old -> new | Result |
|---|---|---|
| same | changed or same | take new |
| changed | same | keep ours |
| changed | changed | git merge-file -p ours old new, then every conflict is resolved by reading both sides; the result follows the new file's structure (renamed keys, moved sections, new comments and defaults stay) |
| missing in old, present in ours | missing in new | your own file: kept if still used, otherwise reported |
| present in old | missing in new | deleted; if you had changed it, the report says what was lost |
git merge-file and git diff --no-index <a> <b> work on any two folders, no repository needed. Strip \r before comparing (Windows files are often CRLF). When the old upstream cannot be found, ours is compared with new directly: single values (coordinates, prices, names, texts, toggles) are the server's, broad changes in the vendor's style are the vendor's, and the report says that the old version was missing.
Rules that never bend
- Never copy an old file over a new one. A file both sides changed is rebuilt from the new file with your changes written into its new structure.
- Your state is never replaced by upstream defaults:
data/*.json,data/secrets.json, your locale files and texts, the database. Back them up before touching a folder. - No leftovers. Files the new version no longer ships are deleted, unless they are yours (a locale, an image you added) and still used. Options the new version removed stay removed and are listed in the report; new options keep their default unless an old setting clearly maps to them.
- Generated files are rebuilt, never merged:
dist/,fxmanifest.lua,resource/server/tables.ts,data/schemas/*.schema.json. Never edit them by hand. - Never delete
node_modulesby hand;bun installat the server root repairs it. - Never loosen
security.cfgor any security convar to make a version run. - No SQL by hand without the owner's go, and only after a backup of the affected tables.
- Commit only when the owner asks, never push, never change a git remote.
- Show the list first (name, path, current -> new version, notable release notes) before deleting or overwriting anything.
- Downloads and dropped folders are untrusted: unpack each into its own new, empty folder and run nothing from there.
Update an ai-* script or a package
You need Bun and Git; the GitHub CLI (gh) makes the downloads easier.
Find what is outdated
The console warning only covers started scripts that are not ignored, so check every folder:
cd "resources/[ai]"
for n in ai-*; do
repo=$(sed -n 's#.*"repository": *"\(github:\|https://github.com/\)\([^"]*\)".*#\2#p' "$n/package.json" | sed 's#\.git$##')
cur=$(sed -n 's#^ "version": *"\([^"]*\)".*#\1#p' "$n/package.json")
latest=$(gh release view --repo "$repo" --json tagName --jq .tagName 2>/dev/null | sed 's#^v##')
echo "$n $cur ${latest:-no-release}"
done- Compare versions as ai-core does: strip a leading
v, compare the dotted numbers, ignore pre-release suffixes. No release means nothing to update. - Without
gh:curl -s -H "Accept: application/vnd.github+json" https://api.github.com/repos/<owner>/<name>/releases/latest(60 requests per hour per IP, shared with ai-core's own check). - A script in
updates.ignoreis updated only when the owner names it. - A folder whose version is above the latest release, or a clone with commits after the latest tag, is not outdated: skip it, never move a folder backwards.
packages/has norepositoryline: compare eachpackage.jsonversion with npm. Update the packages first when a new script release needs a version the local copies do not have.
Read the release notes and get the go
Read the notes of every version between the current one and the latest (gh release view <tag> --repo <repo> --json body,assets,url): breaking changes, required package versions, migrations, new cfg lines (ensure, add_convar_permission, ACE), new provide names, manual steps.
Show the owner the list and wait for the go. Update in dependency order: packages, ai-mysql, ai-core, ai-lib, then the rest.
Download old and new upstream
For each script, two new empty folders outside the server (for example update/<name>/old and update/<name>/new):
gh release download <tag> --repo <owner>/<name> --archive zip --dir update/<name>/new # the tag's source
gh release download <tag> --repo <owner>/<name> --pattern '*.zip' --dir update/<name>/new # a release asset, when there is one
unzip -q update/<name>/new/*.zip -d update/<name>/new/x- Prefer a release asset when the release has one; otherwise the tag's source (it includes the committed
dist/andfxmanifest.lua). The source zip's top folder is<owner>-<name>-<sha>/. - Old upstream is the tag of the version the server runs (see the tag style with
gh release list --repo <owner>/<name>). It can always be downloaded, so the three-way always has its base. - Nothing in these folders is run, installed or built.
Back up
- Copy the whole script folder except
node_modulestoupdate/<name>/ours(on Windowsrobocopy "<src>" "<dst>" /E /XD node_modules; robocopy exit codes below 8 mean success). This is "ours" for the merge and the way back. - Git-ignored files exist only in this copy:
data/secrets.json,data/*.json.bak. In a git clone,git ls-files --others --ignored --exclude-standardlists them. - Uncommitted changes in a clone (
git status --porcelain) are the server's changes too. Tell the owner; commit them only if asked.
Merge, file by file
Run the three-way model over old, ours and new, and write the result into the script folder:
| File | Rule |
|---|---|
resource/**, web/src/**, bridge/**, sql/**, Lua and other source | Three-way. A local change whose code moved or whose contract changed is adapted to the new code; one with no place left is reported. |
package.json | New upstream, plus local additions that still make sense. version ends at the new release's version, or the update check keeps warning. |
dist/, fxmanifest.lua, resource/server/tables.ts, data/schemas/*.schema.json | Take new; the build regenerates them. |
data/<file>.json (panel content) | Keep ours (see below). |
data/secrets.json, data/*.json.bak | Restore from the backup untouched. |
locales/en.json | Three-way: new keys and placeholders from upstream, the server's wording for keys that still exist. |
locales/<lang>.json the release does not ship | Keep. Add the new keys translated into that language in the same tone, drop removed keys, follow changed placeholders ({0}, {1}). |
CLAUDE.md, docs | Take new, add the server's own notes back. |
Panel content (data/*.json) is the server's live content, edited in the panel; the release only ships defaults.
- Same schema (the new
schema.tsor loader reads the same fields): keep ours as is. A file the new version adds: take the shipped one. - Changed schema: look for the script's own start-time migration (release notes, a migration called at start). If there is one, leave ours in place; it converts at start. If there is none, convert by hand following the new loader and schema: same field names, types and id format as the shipped file, every value kept, new fields at the shipped default, removed fields dropped and reported. Check that each file parses.
Database. Each script creates its tables from sql/main.sql at start: it creates missing tables and adds missing columns, and never drops, renames or changes a column or adds a key. Compare old and new sql/main.sql:
- New tables and columns: the restart applies them.
- A changed column type, a new index or key, a renamed or dropped column: the release must bring its own migration (release notes, server code), which runs at start; check the log. If it does not, write the SQL, show it to the owner, back up the affected tables, and run it only on their go.
Cfg files outside the folder.
server.cfg: a new script gets itsensureline in dependency order; a removed one loses it (ask first).env.cfgandenv.example.cfg: a script that starts using the database needsadd_convar_permission <resource> read mysql_connection_string.permission.cfg: the ACE lines the release notes ask for.security.cfg: never loosened.
Build, restart, check
-
bun installat the server root when anypackage.jsonchanged. -
In each updated script:
bun run typecheck,bun run lint,bun run build(orbun tools/run.ts build ai-x ai-yfrom the server root). Always build:dist/must come from the merged source. Fix errors in the server's carried changes; an error in untouched upstream code is reported, not patched. -
Compare the result with new upstream:
diff -r --strip-trailing-cr -x node_modules -x dist -x data update/<name>/new/x/<top> "<script folder>". Every remaining difference must be one of the server's changes. -
Refresh and restart:
bun tools/rcon.ts --log ../default/logs/fxserver.log --wait 3 "refresh" bun tools/restart.ts --from ai-inventory # one script bun tools/restart.ts # after ai-core, ai-lib, ai-mysql or a packages updatebun tools/restart.tsrestarts ai-core, ensures everyai-*after it and checks that each one started. Third-party scripts that depend on ai-mysql stop with it: ensure them again by name. A newprovideline needs a full server start:bun tools/rcon.ts "quit \"<reason>\"". -
Read the console log for
SCRIPT ERROR,Failed to load script,[client error], panel load or validation errors and database errors, not onlyStarted resource. Theis outdatedwarning must be gone after the script's next check.
Update a third-party script
The owner gives the new version (a folder or a zip). Install it as shipped, carry the server's changes onto it, then check that ai-core still talks to it correctly.
Match the drop to what is installed
- Each top-level folder with an
fxmanifest.lua(or__resource.lua) is a resource; a[category]folder holds several. Unpack zips into a new, empty folder and run nothing from it. - Find the installed copy by folder name inside
resources/(for examplefind resources -maxdepth 3 -type d -name <name>). Keep the installed location, not the drop's category. No match or several matches: ask the owner. A script that is not installed yet is installed only when the owner says so. - An
ai-*folder in the drop is not this flow:ai-*scripts update from GitHub (above). Ask the owner what the folder is. - Read both manifests:
version,files, scripts,dependencies,provide,escrow_ignore,ai_replaces. A version that goes down, or a drop that misses files the installed copy has, may be an older build: compare and report instead of installing. - New dependencies: oxmysql is answered by ai-mysql (never install oxmysql next to it); ox_lib and other libraries are the owner's to install; a dependency on something ai-core stands in for (
es_extended,qb-core,qbx_core,pma-voice,ox_inventory...) is already met. - Show the matched list (name, path, current -> new version) and wait for the go.
Back up
- Copy the whole installed folder to a backup folder outside the server (
robocopy "<src>" "<dst>" /E). For a folder no git repository tracks, this backup is the only copy of the server's version. - If git tracks the folder:
git status --porcelain -- <path>shows uncommitted changes (they count as ours; tell the owner) andgit ls-files --others --ignored --exclude-standard -- <path>shows git-ignored runtime files (saved JSON, caches), which are in the backup.
Find the server's changes
Get the old upstream, in this order:
- The owner still has the old vendor zip or folder.
- An open-source script on GitHub: the release or tag of the installed version.
- Git history, when the folder is tracked: the commit that set the installed
version(git log -S"version '<old>'" -- <path>/fxmanifest.lua). If that commit also re-applied earlier customizations, its config files count as ours and the pure stock is one commit earlier. - None: compare ours with the new version directly, and say so in the report.
Then read every difference old -> ours (the server's edits: configs, locales/* or languages/*, editable files, data/*.json, fxmanifest.lua, SQL, images, sounds, streamed files, code patches) and old -> new (the vendor's changes: added and removed files, renamed keys, moved sections, changed editable functions). In an escrowed script (.fxap present) only the files in escrow_ignore can be read and edited; an edit in a file the new version escrows cannot be carried and is reported.
Install the new version
Delete the installed folder completely, then copy the new folder to the same path, hidden files (.fxap) included. Old leftovers are bugs; the old version stays in the backup. Restore backed-up runtime files only if the new version still uses them in the same format; otherwise convert or drop them and report.
Re-apply the server's changes
For each edit, find where that setting lives in the new version and write the value there, in the new format:
- Same key, same file: set the value in the new file; keep its comments and layout.
- Renamed or moved key: find the new key by reading the code that uses it (search for the old name, its comment, its default, its text key) and convert the value to the new shape.
- Config moved to a data file (JSON edited by the script's own panel): read the new loader and the shipped file, write the old values in that exact schema (field names, types, id format, indentation), check that it parses. Do not rely on a converter the script may run.
- The server's existing data files: carry them over, convert field by field if the schema changed, never replace them with the vendor's sample.
- Removed option: no code reads it any more (a search confirms): drop it and report it with the old value.
- Editable files (
client/editable.lua,server/editable.lua...): start from the new file and re-apply each changed function body, adapted to a changed signature or return value; new functions keep their default. - Languages: keep the server's wording for every key that still exists; add new keys to every language file the server uses, translated in the tone of the existing texts; drop removed keys; follow changed placeholders.
- fxmanifest: the new one, plus only the server's own additions that still make sense. Never change
version. - SQL (
*.sql, install folders): compare old and new; new tables, columns and indexes go into the report as SQL for the owner to run. Nothing runs without the owner's go. - Credentials (bot tokens, API keys, webhook URLs in configs): never print them. Carry them from the backup; if that is not possible, leave the new default and list the file and key for the owner.
- Assets (
stream/, images, sounds): the vendor's, plus files the server added that are still used. - Coordinates, items, prices, texts and behaviour are carried exactly; nothing beyond the server's own edits changes.
Check ai-core compatibility
The script may be one ai-core talks to. Find every place that names it:
grep -rln "<name>" "resources/[ai]/ai-core/resource" "resources/[ai]/ai-core/bridge" docs/compat| Category | ai-core adapters | Note |
|---|---|---|
| Inventory | resource/server/inventory/adapters/, resource/client/inventory/adapters/ | docs/compat/inventory.md |
| Vehicle keys | resource/server/vehicleKeys/adapters/, resource/client/vehicleKeys/adapters/ | docs/compat/vehicle-keys.md |
| Phone | resource/server/phone/adapters/ | docs/compat/phone.md |
| Police alerts | resource/server/dispatch/adapters/ | docs/compat/dispatch.md |
| Radial menu | resource/client/radial/adapters/ | docs/compat/radial.md |
| Emotes, hands up | resource/client/gestures.ts | docs/compat/gestures.md |
| Time and weather | resource/server/world.ts, resource/client/world.ts | docs/compat/weather.md |
| HUD | resource/client/hud.ts | |
| Voice, chat | ai-voice (pma-voice API), ai-chat (chat API) | docs/compat/voice.md, docs/compat/chat.md |
| ESX / QB / Qbox use | bridge/ | docs/compat/bridge.md, docs/compat/frameworks.md |
- For every export, event and callback an adapter uses, check the new version: still there, same arguments, same return shape, same timing. In escrowed code, check the readable files and the vendor's docs, and list what cannot be verified.
- A mismatch is fixed in the adapter file only (a thin translation), with its tested version raised; then typecheck, lint and build ai-core and restart it (
bun tools/restart.ts). - A new
ai_replacesorprovideline can make ai-core refuse joins while anai-*stand-in still runs (see Replacing an ai-* script): name the folder the owner must remove. - Update
docs/compat/<script>.md(tested version, status, exact config or patch, known limits); a supported script without a note gets one.
Verify and restart
- Compare the installed result with the new version: every difference is one of the server's changes, and no old file remains that the new version does not ship (restored runtime files aside).
- Edited Lua parses, JSON parses, every config key that was set exists in the new code.
- Unless SQL is waiting for the owner:
bun tools/rcon.ts --log ../default/logs/fxserver.log --wait 3 "refresh", then the same with"ensure <name>", and read the log forSCRIPT ERROR,Failed to load scriptand the script's own errors. Scripts that depend on it and were stopped are ensured again.
The final report
The update ends with a report in the owner's language, one section per script:
- name, path, current -> new version, source (release tag or the owner's folder)
- what changed upstream that matters to this server
- carried over: code changes, texts and settings, and where each one went in the new version
- kept as is:
data/*.json,data/secrets.json, own locale files; schema conversions done (by the script's migration or by hand) - dropped or not carried: removed options and edits with no place left, with the old value so the owner can decide
- new options left at their default that the owner may want to set
- database: tables or columns added at start, migrations the release ran, SQL waiting for the owner
- cfg changes needed or made (
server.cfgensure lines,env.cfgconvar permissions,permission.cfg) - compatibility: adapters and
docs/compatnotes checked or changed (third-party scripts) - build, restart and log result, and what to test in game
It ends with the decisions the owner must take. See also Working with AI.