Code agent (Syntax)

Updated Oct 15, 2026 Open the tool

The Code agent at /app/code is an AI assistant called Syntax that builds and edits FiveM resources (Lua scripts and NUI web interfaces) inside a project workspace, charging credits per message based on the model used. In /app/code, files and resources also open their menu on right-click (Open, Delete), since 2026-09-22.

What the Code agent builds

The Code agent lives at /app/code and is named Syntax. The page title is "Code Agent" and the page description reads "Generate, edit, and explain code with Zoovdev." Any signed-in user can open it; signed-out visitors see a login overlay. Syntax works inside a project: you describe what you want ("A garage system with NUI that lets players store vehicles..." is the placeholder) and it scaffolds files, edits them, and iterates. The welcome text says "Describe what you want to build and I'll scaffold, code, and iterate on your project."

Each project targets one FiveM framework: "ESX" (ESX Legacy), "QBCore", "QBox" (qbx_core) or "Custom" (standalone or other). QBCore is the default. Projects also declare dependencies. Four are pre-selected and can be toggled off: ox_lib, ox_inventory, oxmysql and ox_target. You can add up to 10 "Additional Dependencies" by URL (placeholder https://github.com/user/repo).

Syntax has tools to list, read, create, edit, multi-edit, delete and rename files, fetch framework docs, search the web, fetch a URL, create and restore snapshots, read context documents, save memories, grep files, look up FiveM natives, sync fxmanifest.lua, lint FiveM code, validate event bridges, scan exports and check dependencies. QBCore projects also get a QBCore docs tool. The chat shows these as labels such as "Create File", "Edit File", "Fetch Docs", "Search Web" and "Save Memory".

Projects, files and limits

Projects are created from the left "Projects" sidebar with the "New Project" button, which opens a three-step "Create Project" dialog: "Resource Name" (placeholder my_fivem_resource, up to 80 characters), "Description" (up to 500 characters), framework, then dependencies. The button "Create Project" finishes.

Limits enforced by the server:

  • 10 projects per account. Error: "Maximum of 10 projects allowed".
  • 50 files per project. Error: "Maximum of 50 files per project".
  • 256 KB per file. Error: "File too large (NKB). Maximum is 256KB."
  • File paths up to 200 characters, no .., no absolute paths.
  • 20 project creations per hour and 20 project deletions per hour.

The editor has a file explorer, a code editor with "Save (Ctrl+S)", a diff viewer, a "Node graph view" ("Code Graph") that maps files, dependencies, database use and config values, and a "NUI Preview". Deleting a project asks "Delete this project and all its files?" and cannot be undone. Deleting a file asks "Delete ?".

Uploading an existing resource

In the workspace (/app/code, since 2026-09-28) a ZIP can also be dropped on the middle of the tool, or chosen with Upload a ZIP under "Pick a resource": New resource opens on its From a ZIP side with the file already in it and the name filled from the file. The Pro rule below is the same.

The "Upload Resource" button in the sidebar imports a ZIP as a new project. Uploading requires the Pro tier or higher (the subscription or the Discord role, whichever is higher, since 2026-09-22; the chat's paid rate and premium models follow the same rule, see plans-and-subscriptions.md); otherwise the server answers "Project upload requires Pro." The dialog says "Drop your ZIP here" and "or click to browse -- max 5MB, text files only".

Upload rules:

  • .zip only ("Only .zip files are allowed"), at most 5 MB compressed and 20 MB uncompressed.
  • At most 50 files ("Too many files. Maximum 50 files allowed."), each at most 512 KB.
  • Text files only. Allowed extensions: lua, js, ts, jsx, tsx, json, html, css, xml, yaml, yml, md, txt, cfg, sql, ini, fxmanifest, vue, svelte. Images, audio, archives, binaries, shell scripts, node_modules and .git are blocked. A blocked entry returns "ZIP contains blocked files" with the first five paths listed.
  • A single top-level folder inside the ZIP is stripped automatically.
  • 8 uploads per hour ("Too many upload attempts. Please try again later.").

If no name is given, the project is named after the ZIP file. Imported projects get the default dependency set.

Context documents and memory

The "Context Documents" dialog lets you upload reference material for Syntax: "API specs, design docs, coding standards, database schemas, or any relevant material." Syntax can also write its own MEMORIES.md there using the "Save Memory" tool so it remembers decisions across conversations.

Rules: at most 20 context documents per project ("Maximum of 20 context documents allowed"), 512 KB each, text only, one flat folder (no subfolders). Allowed extensions include md, txt, json, yaml, yml, toml, cfg, ini, csv, lua, js, ts, html, css, xml, sql, sh, bat, ps1, env, example, log, conf and properties. Context files are stored under .context/ inside the project. They are listed to Syntax at the start of each chat, they are never included in downloads or deploys, and they do not appear in the NUI preview.

Chat, turns and change history

Each message you send starts a turn. Syntax works in steps (tool calls and text). Users with code access get up to 12 steps per reply; everyone else gets 6. Near the limit Syntax is forced to stop and summarise. Every file change in a turn is recorded, and the "Change History" panel (toggle "Toggle change history") lists turns with per-file diffs ("View diff"). A turn can be reverted with "Revert changes"; reverted turns show a "reverted" badge, committed turns a check mark. Turn statuses are pending, committed and reverted. Only the latest 20 turns are listed.

Deleting a file through Syntax is not automatic: the "Confirm Actions" dialog appears and you "Confirm" or "Reject" each pending deletion.

Chat controls: "Stop" while Syntax is thinking, "Clear" to wipe the conversation, and "Discard last response" ("Drop the last (broken) response and retry") if a reply breaks. Chat history is saved per project and the most recent 50 messages are reloaded. If a message fails with "Something went wrong. Try sending your message again.", use "Discard last response" first, then "Clear" if it keeps failing.

Prompt limits by plan: the message box shows a counter like 0/500. Maximum characters per message are 500 (Free, Lite and Booster), 1,250 (Standard and Referrer), 2,500 (Pro) and 4,000 (Max). Until 2026-09-30 Lite and Booster had Standard's 1,250; since then they count as Free here, as everywhere outside the plugin. Over the limit the server says "Prompt exceeds the N-character limit for your plan." Since 2026-10-15 the composer knows the plan's limit (getCodePromptInputCharLimit(page.data.tier)): from 80% of it a {count} / {max} line sits under the box, over it the line turns red with "Up to N characters on your plan · See plans" (plans door code-prompt-limit, Standard highlighted) and the send arrow and Enter do nothing; the box still takes the whole paste (4,000) so nothing is cut. Should the server still refuse (PROMPT_TOO_LARGE, LOW_CREDITS, NO_CREDITS), the store keeps the code as syntax.wall and the panel shows a row with the reason and See plans (code-prompt-limit, or code-credits on the packs) instead of a failed reply; code_reply_failed carries wall so the walls count apart from real failures (they were 23% of "failures" in the month before). The whole conversation plus project context is also capped at roughly 24,000 tokens (Free, Lite and Booster), 48,000 (Standard and Referrer), 96,000 (Pro) and 144,000 (Max); past that you see "Chat context is too large. Start a new chat to continue."

Rate limit: 30 chat messages per 10 minutes with code access, 8 without. Error: "Too many chat requests. Please wait a moment before asking Syntax again."

Improve prompt

The magic wand button next to send is "Improve with AI". It rewrites a rough idea into a structured brief under 200 words without adding features. Input is capped at 200 words ("Prompt exceeds 200 word limit"), limited to 30 uses per hour, and always uses GPT-5.4 Nano. It is billed like any other message, so it needs a positive balance ("Insufficient credits").

Model choices

The model selector (a chip in the composer in the workspace, the select in the chat header on the legacy page) lists these models since 2026-09-19. Prices are the underlying API rates per one million tokens (input / output) and drive credit cost. The usage label is what the selector shows.

  • GPT-5.6 Luna (default): $0.20 / $1.20. "Light usage". "Everyday coding model, quick and cheap".
  • GPT-5.6 Terra: $2 / $12. "Moderate usage". "Stronger on larger resources and refactors".
  • GPT-5.6 Sol: $4 / $20. "High usage". "Hard tasks that need the most careful model". Code access only. (OpenAI's promotional price, held at least until 2026-11-21; $5 / $30 at launch.)
  • GPT-6 Astra: $10 / $50. "Very high usage". "The flagship, for the hardest work". Code access only.

GPT-5.6 Sol and GPT-6 Astra are reserved for users with code access (Standard, Pro, Max or Referrer role). If someone without code access selects them, the server silently uses GPT-5.6 Luna instead. All four have a one-million-token context. Syntax runs them at low reasoning effort through OpenAI's Responses API (reasoning tokens are billed as output tokens, so the effort is kept low); prompt improvement and the NUI test-message helper run GPT-5.6 Luna with no reasoning. The models offered before (GPT-5.4, 5.4 Mini, 5.4 Nano, 5.5, 5, 5 Mini, 5 Nano) are no longer in the list; their pricing rows stay so old usage still reads, and the GPT-5, 5 Mini and 5 Nano rows are inactive because OpenAI retires them on 2026-12-11.

Credit pricing per message

Credits are charged after each reply from the real token count, not up front. One credit equals $0.01 of API cost. The cost is what OpenAI bills: input read back from the prompt cache at a tenth of the input rate, the rest of the input at the input rate (on GPT-5.6 and GPT-6 that rest is a cache write, billed at 1.25 times the input rate), and output at the output rate. It is converted to credits, rounded up, minimum 1 credit, then multiplied by a plan multiplier:

  • Code access (Standard, Pro, Max, Referrer): multiplier 2 since 2026-10-12. It was 0.8, but the cost then priced every input token at the full rate. A reply's steps resend the conversation and mostly read it from the cache, so a typical reply costs about what it did; a reply that reuses little of the cache no longer costs Max more than it charges (a credit sells for $0.0083 on Max yearly, so a reply clears 1.66 times its cost there and 2.66 on Pro yearly).
  • No code access (free, Lite, Booster) with more than 50 credits: multiplier 4.
  • No code access with 50 credits or fewer: multiplier 8, and replies are capped at 2,000 output tokens.

Example: a one-step GPT-5.6 Luna reply using 20,000 input (none of it cached yet) and 3,000 output tokens costs about $0.0086, which is 1 credit raw, 2 credits with code access, 4 without. The same reply on GPT-5.6 Terra costs about $0.086: 9 credits raw, 18 with code access, 36 without. Later steps of the same reply read the earlier conversation from the cache, so a five-step reply costs far less than five first steps.

Replies are also capped at 16,000 output tokens with code access and 6,000 without. Before a reply starts, the server checks you can afford at least 2,000 output tokens (500 when low-credit limited) after reserving 4 credits (2 when low). If not: "Add credits or upgrade to continue using Syntax." A reply may take the balance slightly negative: down to -50 with code access, -10 without, -5 when low-credit limited. Since 2026-10-12 a reply is told to wrap up once it has spent three quarters of that room (balance plus the allowance) and is stopped when it has spent all of it, and a reply that still costs more is charged down to the limit (uncollectedCredits in the usage row's metadata). Until then the budget counted the first step only, a long reply could run past the limit, and the whole charge then failed, so the reply cost nothing. With code access you can still start a chat while in debt above -50; without it you need a positive balance. Below zero the UI shows "You're out of credits" with "Add credits" and "Upgrade" buttons. Between 1 and 50 credits without code access it shows "Credits are running low. Upgrade for lower Syntax usage, or add credits to continue." Everyone without code access sees "Standard+ uses fewer credits per message with Syntax."

Code in the workspace (/app/code, 2026-09-18)

Since 2026-09-18 the Code agent also runs inside the workspace as Code, under Create, when the game at the top of the rail is FiveM. It is marked Beta. The old page at /code stays up, and the two share everything on the server (the same /api/v1/code routes, projects, files, turns and conversations), so a resource started on either page is the same resource on the other.

The owner's brief for the move is a methodical one: bring it over piece by piece onto the workspace's system, and improve it on the way rather than copy it. The three improvements the brief named were built on 2026-09-19: going back to an earlier prompt (Rewind, below), a proper build for React and TypeScript NUI (see NUI build under NUI preview), and a better tool set for Syntax (below). What is there now:

  • Signed out. The page renders; the resources list shows a card ("Sign in · Your resources · Discord", primary Sign in with Discord), New resource and the composer's arrow go to sign-in, and nothing loads until a session exists. Before 2026-09-19 a signed-out visitor saw the loading ghosts for ever, because the load effect re-ran on its own failure.
  • Resources. The left panel lists the account's resources, newest first, with a search and New resource (name, what it does, framework: QBCore, ESX, QBox or Custom). Ten per account, as before. A resource's menu deletes it.
  • Files. Opening a resource shows its files as a tree (since 2026-09-19): folders fold and unfold with a chevron and show how many things they hold, folders sort before files, the .context folder is named Context and sits last, and every file wears a glyph for what it is (code, data, page, styles, text). The count out of 50 is in the caption and a New file button beside it. The file in the editor is marked with a raised row the same frame it is pressed, before its text arrives; an amber dot on it means unsaved work. A file's menu deletes it. Opening a resource opens fxmanifest.lua first when there is one.
  • The stage has three views, switched by a segmented control in the toolbar: Editor, Preview (the NUI running) and Graph (the map of the resource), the second two ported on 2026-09-19. When a reply gives a resource its first page, the stage switches to Preview on its own, so someone who does not read code sees the result.
  • Preview runs the resource's page the way the legacy NUI preview does, ported: every html, css and js file is read, inlined into one document (React sources are compiled in the browser as before) and run in a sandboxed frame over a GTA screenshot, with Run, Refresh and Stop, the viewport presets (16:9, 4:3, 9:16, Fill, or a corner drag for a custom size) and the size readout in a control row, and a tools column at the right (toggled by the panel icon, open on wide stages) with the detected events as chips, the SendNUIMessage box, Suggest messages · charged (the AI helper, GPT-5.6 Luna), the callbacks the page posts back, and the console. Empty state: "No html file yet · ask Syntax for a UI".
  • Graph is the legacy node graph, ported: one card per file in columns (Config, Client, Shared, Server, Dependencies, Database, UI) with the symbols as ports and the events, calls and imports drawn between them; config cards edit their values in place and Save writes the file through the same config route. Relayout and Fit sit in a floating row with the count ("3 files · 1 connection"); the map follows the light and dark toggle and never shrinks below 80% so it stays readable, panning instead.
  • The editor. The Editor view is the file in Monaco, filling the viewport in Fira Code, with the toolbar floating over its first lines. Pressing another file marks it at once and veils the old text with a running hairline until the new text lands, so a slow file never shows under the wrong name. Its colours are the workspace's own tokens read at open and again when the light and dark toggle flips, so the editor is the panels' slate on either side. The toolbar names the file and says "unsaved" while it is; Save writes it, and so does Ctrl+S. History (since 2026-09-19 a sheet floating at the right edge of the editor) lists the turns Syntax took newest first with a count in its head, each with its prompt, time and file count and an Undo that puts those files back (the legacy page's revert); a turn already put back is struck through and reads "Put back".
  • Syntax. The third panel is the conversation. Before anything is said it offers four example briefs (a garage with a menu, a /car command with a cooldown, a NUI job menu, saving outfits to the database); pressing one fills the box and sends nothing. A reply shows the tools it uses as it uses them ("Writing a file", "Reading the framework docs", "Looking up a native") and its text; the file list and the open file refresh as files land, and again when the reply ends. While a reply runs, a sparkle breathing on the AI gradient and a word with light passing through it say what Syntax is doing right now (Thinking, then the tool it reached for, then Writing); there is no progress bar, because a reply has no length. The composer (since 2026-09-19) is one bar, the workspace's lit AI field, with everything about the message inside it: the text (Enter sends, Shift+Enter for a new line), and under it a + menu (Add a document: a text file up to 512 KB goes into the resource's .context folder for Syntax to read; Improve the prompt), the model as a chip that opens the model list with each one's usage band, the wand (the legacy Improve with AI), and a round arrow that sends; while a reply runs the arrow becomes a square that stops it. The eraser in the panel head asks before it clears the conversation ("Clear the conversation · The messages are removed here and on the server. The files stay.") and then removes it here and on the server. The caption under the bar names the model's usage band and that replies are charged per reply.
  • Rewind to a prompt (2026-09-19). Every prompt in the conversation has a rewind button beside it (it shows on hover). Pressing it asks first ("Rewind to this prompt"), then puts the resource's files back to how they were before that prompt was sent, removes that prompt and everything after it from the conversation, and puts the prompt's words back in the box so they can be changed and sent again. It is not the same as Undo in the History: Undo puts one turn's files back and refuses any file a later turn also touched; a rewind puts every turn from that prompt on back in one pass, whatever touched what. The turns show as "Put back" in the History afterwards. Prompts sent before 2026-09-19 do not know which turn wrote their files, so rewinding to one of those cuts the conversation and leaves the files alone.
  • The conversation follows the reply (2026-09-19). The conversation scrolls on its own to keep the newest line in view while Syntax writes. Scrolling up to read lets go of that: the view stays where it was put, and a round arrow appears at the bottom of the conversation (tinted while a reply is still running). Pressing it, or scrolling back to the bottom by hand, returns to the newest line and the conversation follows again. Sending a prompt always brings it into view.
  • While a rewind runs (2026-09-19). From the moment Rewind is confirmed until the files are back, the prompt and everything after it dim, the sparkle reads "Rewinding", the editor sits under a veil that says "Rewinding", and the file list, the toolbar, the conversation and the composer cannot be pressed. Pressing things in the middle of a rewind used to be possible and could leave the conversation and the files out of step.
  • The open resource is in the address (2026-09-19). Opening a resource moves the address to /app/code/<resource id>. Reloading the page comes back to that resource (the editor says "Opening" while the list loads), a link to it opens it, the browser's Back from a resource is the list and Forward opens it again. A link to a resource that is not on the account says "Resource not found" and lands on the list. Leaving Code for another tool and coming back returns to the resource that was open.
  • A reply survives a reload or a lost connection (2026-09-19). Once a prompt is sent, Syntax finishes the reply on the server whether or not the page is still there, and the server keeps the conversation (the prompt from the moment it is sent, the reply when it ends). Reloading mid-reply, closing the tab, or losing the internet does not lose the work or the credits' worth of it: coming back shows the prompt with the sparkle still going ("Working", then "Writing " as files land, "Waiting for the connection" while offline), the file list fills in as it goes, and the finished reply appears when it ends. If a reply really did end early there is a row under the conversation: No reply came back · Try again (the prompt is sent again) or Reply stopped early · Continue (Syntax is asked to carry on from where it stopped). Stop still stops: it asks the server to end the reply, and a stopped reply is charged for the steps it finished. The legacy /app/code page keeps its old behaviour (a reload there still cuts the reply).
  • Syntax asks before it deletes (2026-09-19). When a reply wants to delete a file, a dialog names the file with Keep it and Delete; nothing is removed until Delete is pressed, and a deleted file comes back with Undo on that turn.
  • A ZIP as a new resource (2026-09-19, Pro and Max). New resource has two ways in: From nothing, and From a ZIP (up to 5 MB, 50 text files, the same rules as the legacy Upload Resource). The name defaults to the ZIP's. Below Pro the ZIP side says so and offers the plans.
  • Deploy (2026-09-19, Pro and Max). The rocket beside Download opens Deploy: pick a linked server (its state beside the label: Online, Offline, Needs linking again), choose whether to start the resource after, press Deploy. Since later the same day it is the one deploy dialog the whole workspace shares (src/lib/workspace/deploy/, see fivem-servers, "Deploying from the workspace"), used for model resources too and running on /api/v2/deploy: the servers are rows with their state, a server is linked inside the dialog in four steps rather than on the legacy page, each step of the deploy is named as it runs, and a server the site cannot reach gets the manualsync <id> command to copy. Below Pro it offers the plans. The Code tool no longer has a Deploy.svelte of its own.
  • Syntax's new tools (2026-09-19). projectMap gives Syntax the whole resource in one step (every file with its side, its commands, events, exports and callbacks, and which file's event another handles; the same analysis the Graph view draws), and readFiles reads up to eight files in one step. Replies run on a limited number of steps, so a change across client, server and config no longer spends half of them reading. scaffoldNui and buildNui are described under NUI build.
  • Download packs the resource as <name>.zip, as the server does.

Not in the workspace yet, still on /app/code: the list of context documents (adding one works from the composer) and linking a FiveM server.

Newer models, researched 2026-09-19. OpenAI has released four chat models since the pricing rows were last written, and the site's key can see all of them: GPT-5.6 Luna ($0.20 in / $1.20 out per million tokens), GPT-5.6 Terra ($2 / $12) and GPT-5.6 Sol ($4 / $20, a promotional price OpenAI says holds at least until 2026-11-21; $5 / $30 at launch), all from 2026-07-09, and GPT-6 Astra ($10 / $50) from 2026-09-03. All four have a 1M-token context and tool calling. On a typical Syntax turn (about 16,000 tokens in, 3,000 out) that is roughly 1 credit for Luna, 6 for Terra, 10 for Sol and 25 for Astra at cost, against 3 for GPT-5.4 Mini, 7 for GPT-5.4 and 14 for GPT-5.5 today, before the plan multiplier. Luna is about a quarter of the price of GPT-5.4 Mini, the current default, and OpenAI's published coding scores put it above GPT-5.5. Three things stand in the way, all checked with a real call on 2026-09-19: the 5.6 models and Astra only take tool calls with reasoning through the Responses API (Chat Completions answers "Function tools with reasoning_effort are not supported"), and the chat route talks to Chat Completions, so the route has to switch; they reason at medium effort by default and reasoning tokens are billed as output, so Syntax should set the effort low; and the GPT-5, GPT-5 Mini and GPT-5 Nano snapshots shut down on 2026-12-11, so those three rows (and the NUI test-message helper, which uses GPT-5 Nano) have to move before then. Applied on 2026-09-19 with the owner's go-ahead: the four rows are in ai_model_pricing, the GPT-5 family rows are inactive, the chat route uses the Responses API at low effort, and the model list on both pages is the four above.

Prices, rechecked 2026-09-18. Every active row in ai_model_pricing (GPT-5.5, 5.4, 5.4 Mini, 5.4 Nano, 5.2, 5, 5 Mini, 5 Nano) matches OpenAI's current standard-tier rate exactly; the rows were last written on 2026-04-26 and nothing has moved since. Nothing was changed. One thing the billing does not do: OpenAI charges a fraction of the input rate for a prompt prefix it has cached, and Syntax resends the same project context every turn, so a good share of input tokens are cached on OpenAI's side while the ledger bills them at the full input rate. The margin lands on the site; whether to pass some of it on is the owner's call and is not decided.

Download and deploy

"Download Resource" zips the whole project with the project name as the root folder, excluding .context/. It works on any plan, limited to 30 downloads per 10 minutes. Empty projects return "Project has no files".

"Deploy" (the rocket beside Download) opens the workspace's Deploy dialog and sends the resource to a linked FiveM server. Deploying needs Pro or Max; below Pro the dialog says "Deploying is part of Pro and Max" and offers the plans. zoov.dev packs the project, stages it in storage under your account and hands it to the server through a link that lasts 15 minutes. With "Start it after" on, the resource starts or restarts once it lands; success reads "Deployed". If the site cannot reach the server, the dialog says "The site could not reach the server, so the server fetches it. Run this in its console within 15 minutes." with a manualsync <id> command to copy. Linking a server happens inside the same dialog. The old "Deploy Resource" modal went with the legacy /code page on 2026-09-26, and its route on 2026-10-13. See the FiveM server doc for linking and troubleshooting.

NUI preview

"Preview NUI" switches the editor to "NUI Preview", which runs the project's HTML, CSS and JavaScript in a sandboxed frame in the browser, over a random GTA location screenshot as background. React projects get a "React" badge. Controls: "Run preview", "Refresh preview", "Stop preview", viewport presets (fill, aspect ratios, "9:16 portrait"), and a resizable frame.

The side panel simulates the game side of NUI:

  • "Detected Events" lists message names found in the source; click one to send it.
  • "SendNUIMessage" lets you paste JSON (default {"action": "show", "data": {}}) and send it to the UI as if from Lua.
  • The "AI" button ("AI: Generate test messages from source code") asks a small model to propose 2 to 8 test messages under "AI Suggestions". It uses GPT-5.6 Luna (GPT-5 Nano until 2026-09-19), is billed like a chat message, and is limited to 20 runs per hour. Only js, jsx, ts, tsx, html and css files (first 20) are analysed.
  • "NUI Callbacks" shows what the UI posts back (fetch to the resource), with default fake responses.
  • "Console" shows logs, warnings and errors from the page, with "Clear console".

Background images are fetched at most 20 times per hour and cached locally for one hour.

NUI build for React and TypeScript (2026-09-19)

FiveM's browser cannot run JSX or TypeScript. Until this build existed, a React interface looked right in the preview (which compiles it in the browser for its own frame only) and arrived in game as raw source that did nothing. The build turns the sources into files FiveM can run. It is free: it uses no credits, because it runs a bundler, not a model.

What the user sees, in the workspace preview at /app/code: the preview builds by itself. When the Preview is opened on a resource with a React or TypeScript interface and there is no built copy yet, or a source file was saved after the last build, the frame area shows a moving line and "Building", then runs the built files (the status chip says "Bundle"), which is exactly what FiveM will run. Nobody has to know a build exists. While Syntax is still writing a reply the area reads "Waiting for Syntax" and the build goes out once, when the reply ends, so a reply that writes ten files causes one build, not ten. One automatic build is made per project and newest source save: a build that fails or is refused is not asked for again until a file changes or Build is pressed, so it can never loop.

If the build fails, the broken page is not shown. The frame area reads "Build failed" with a Build button to try again, and the control row shows in red the first error with the file and line as a button that opens that file at that line ("App.tsx:12"; "· +2" when there are more errors). If builds were refused, the area and the row read "Too many builds · wait a moment" (30 builds per 10 minutes; wait and press Build). Other lines: "Could not build", "Could not build · network", "Signed out", and "Not built" in the rare case nothing was attempted. The Build button (hammer icon) stays in the control row for a manual build, shown when the resource has JSX or TSX files, a ui/src/main entry, or a page whose entry is a <script type="module" src=...>; after a build the row reads "Built · 184 KB · 2 warnings". "Out of date" appears in the row only when a build is owed and has not gone out yet. Syntax also builds on its own after it writes or edits a React interface, reads any errors, fixes them and builds again.

Why it builds instead of compiling in the browser: a starting React interface (what scaffoldNui writes, and what Vite projects look like) has the entry <script type="module" src="./src/main.tsx">. The older in-browser compile cannot run that shape; it filled the console with "Failed to load module script ... MIME type text/html" and "Cannot access '__mod_0_default' before initialization" and showed nothing. That shape is now never given to the in-browser compile. The in-browser compile remains for plain html/css/js interfaces, and for one older React layout in which the server finds no entry to bundle (no ui/index.html with a module script and no ui/src/main), which only the browser compile can run.

What it does: it finds the interface's page (ui/index.html, or the same under html/, web/ or nui/) and its entry, either the page's <script type="module" src="./src/main.tsx"> or, when the page has none, ui/src/main or ui/src/index with a tsx, ts, jsx, js or mjs ending. It bundles the entry and everything it imports into ui/dist/app.js (one minified script for Chrome 100, the floor for FiveM's browser), puts every imported or linked stylesheet into ui/dist/app.css, and writes ui/dist/index.html, the page with its entry script replaced by ./app.js and its local stylesheets by ./app.css. Tags that load something remote (the Tailwind CDN script, fonts) are left as they are; Tailwind still comes from the CDN script because the build runs no PostCSS. Then it edits fxmanifest.lua as text, and only two things in it: the string after ui_page becomes ui/dist/index.html (the line is added at the end if there was none), and 'ui/dist/**' is added to files { } unless a pattern there already covers the three files ('ui/**/*' does). A manifest with no files block gets one. Nothing else in the manifest moves. A plain html/css/js interface needs no build and the build says so ("Nothing to build").

Packages: imports such as react, react-dom, zustand or lucide-react are downloaded from esm.sh while building and included in app.js, so the resource needs no internet in game for them. React and React DOM are version 18.3.1 unless the project says otherwise; a package.json in the project (at the top or in ui/) pins any package through its dependencies block. Without a pin a package comes at its latest version.

Limits, and what the user sees when they hit them:

  • The three built files are ordinary project files. They count towards the 50-file limit, and each must fit the 256 KB file limit. React and React DOM alone are about 147 KB of app.js, so one or two small packages fit and a large component library does not: "ui/dist/app.js is 310 KB; a project file holds 256 KB. Use fewer or lighter packages". When the project has no room: "The build adds 3 files and the project has 48 of 50; delete 1 first." In both cases nothing is written.
  • At most 40 package imports, 5 MB downloaded and 20 seconds per build ("More than 40 package imports", "Packages are over 5 MB fetched", "Build stopped at the 20 s limit").
  • 30 builds per 10 minutes per user (rate limit key nui-build).
  • The built files are not part of a turn's change history (a 150 KB minified file is not a useful diff), so undoing a turn restores the sources and the manifest but leaves ui/dist as it was; the preview then reads "Out of date" until the next build. The manifest edit is part of the turn when Syntax runs the build.
  • Downloads and deploys include ui/dist like any other project file. The sources under ui/src are included too; they are harmless in game because the manifest only serves ui/dist.

Syntax has two tools for this. scaffoldNui with style react writes a starting interface (ui/index.html, ui/src/main.tsx, ui/src/App.tsx, ui/src/app.css, ui/src/nui.ts with useNuiEvent, fetchNui and isEnvBrowser, and client/nui.lua with SetNuiFocus, SendNUIMessage, a togglenui command and a close callback); style vanilla writes ui/index.html, ui/style.css, ui/script.js and the same Lua file. It never replaces a file that exists and reports which files it wrote and which it skipped; it writes nothing at all if the template would not fit under 50 files. The starting React page shows itself when it is not in the game, so the preview has something to look at before any message is sent; in game it waits for the setVisible message. buildNui takes no input and returns the written files with sizes, the packages used, what it changed in the manifest, and errors and warnings as { file, line, column, text } (line and column count from 1; at most 10 of each are returned to the model).

Internal:

  • Server: src/lib/app/server/code-projects/nui/bundle.ts (bundleNui: files in, files out, touches no storage; buildProjectNui: reads through getAllFileContents, checks both limits before the first write, writes app.js, app.css, then index.html last so the page's updated_at dates the build, deletes an app.css left by an earlier build when the new one has none, patches the manifest, stores the summary; readLastNuiBuild), nui/manifest.ts (patchManifestForNui, comment-aware so a comma never lands inside a trailing Lua comment, keeps CRLF), nui/template.ts (nuiTemplate, scaffoldProjectNui), tools in tools/build-nui.ts (buildNuiTool, scaffoldNuiTool, registered in tools/index.ts), one paragraph in prompts/system-prompt.ts (buildToolingGuidance). Tests: nui/bundle.test.ts (22, esm.sh mocked).
  • Route: POST /api/v1/code/projects/:projectId/nui/build answers { build: NuiBuildSummary } with 200 even when the build failed (build.ok false, build.errors); 401, 404, 429 (RATE_LIMITED), 500 otherwise. GET on the same path answers { build: NuiBuildSummary | null }, the last build. The summary is stored as a storage object at <userId>/<projectId>/.nui/last-build.json in the code-projects bucket, beside .snapshots/: it is not a code_files row, so it does not count towards the 50 files and is in no download or deploy. config.maxDuration is 60.
  • Safety: esbuild runs with write: false and one catch-all resolver, so no import ever reaches esbuild's own filesystem resolver and nothing on the server's disk can be read into a bundle (an onLoad on the file namespace refuses as a second guard; tsconfigRaw is set so no tsconfig is looked up). Project code is parsed and printed, never evaluated. Imports that climb out of the project with .., disk paths, file:, node: and any URL outside https://esm.sh/ are errors; redirects are followed by hand (5 at most) and each hop must stay on https://esm.sh/. Remote URLs inside CSS (@import, url()) are left in place, not fetched. esbuild is imported dynamically inside bundleNui so that a missing native binary can fail a build but never the chat route, which imports the tool registry.
  • One copy of React (checked against esm.sh on 2026-09-19): esm.sh's ?bundle inlines a private React into react.bundle.mjs and again into jsx-runtime.bundle.mjs, while React DOM's bundle imports react.mjs, which makes three Reacts and every hook throws. So the react package is requested as https://esm.sh/react@<version>[/subpath]?target=es2020 without bundle, and every other package as https://esm.sh/<pkg>@<version>[/subpath]?bundle&target=es2020&external=react,react-dom, which leaves their react and react-dom imports bare so they come back through the same pinned resolver. Verified by running the real bundle of the template, and of a zustand plus createPortal page, in jsdom.
  • FiveM's manifest matcher (MatchFiles in citizen-resources-core) expands a trailing dir/** through dir/* first, so 'ui/dist/**' does reach the files directly inside ui/dist.
  • Preview: nui/build.ts gained buildNuiDistDocument (inlines app.js and app.css into the srcdoc frame with function replacers, because minified code is full of $&; the script goes to the end of the body because an inline script cannot be defer), NUI_DIST_ENTRY and hasModuleEntry (a page with a local <script type="module" src>, read from the attributes, remote sources excluded). In Preview.svelte: namedBuildable (by file names) or moduleEntry (known once the files are read) make buildable; sourceKey is the project id plus the newest updated_at among the sources and package.json; distCurrent is distFresh (no source under the root saved after dist/index.html) or builtKey === sourceKey, the second because rows are dated by two clocks (the database on insert, the server on update) and a skew must not make a good build look old; needsBuild is buildable && !distCurrent. The automatic build is one $effect on active && needsBuild && !building && !syntax.running (syntax from chat.svelte.ts), guarded by autoTried === sourceKey, which runBuild sets for manual builds too. build() mounts the browser compile only when the page is not Vite's shape and either is not buildable by name or the last build failed with entry: null (the server found nothing to bundle); otherwise nothing is mounted and pending drives the hold states in the frame area, which come before "Loading files". A build that lands after the project changed is dropped. lastBuild resets per project, not per tab toggle. Every real build sends track('code_nui_built', { ok, ms, bytes, auto }); a refused request (429, network) is not a build and sends nothing. Stage.svelte passes (path, line) and code.openFile(path, line) reveals it. Tests: nui/build.test.ts (5).
  • Not verified: the build on Vercel itself. esbuild is a runtime dependency with a native binary; it is in dependencies, so the server build leaves it external and the adapter's file tracing should carry @esbuild/linux-x64, but this was only run locally (Windows, Node 24).

Natives and documentation lookup

Syntax checks FiveM native functions before using them. The "nativeLookup" tool searches a pre-indexed table of every FiveM native (name, hash, aliases, parameters with types, return type, description and examples), by exact name, alias, partial name or keyword. A second table holds FXServer runtime globals such as Citizen.CreateThread and Wait with client, server or shared side. Framework documentation pages fetched by "Fetch Docs" and "Fetch URL" are cached in a documentation cache so repeated lookups do not refetch. Users cannot query these tables directly; they are used by Syntax during a reply.

Common questions

Is the Code agent free to use? Anyone signed in can use it, but every reply costs credits. Without a Standard or higher plan the multiplier is 4x the API cost (8x under 50 credits), and GPT-5.6 Sol and GPT-6 Astra are not available. Standard, Pro, Max and Referrer roles pay 2x (0.8x until 2026-10-12, on a cost that ignored the prompt cache; see Credit pricing per message).

Why did my reply use a different model than I picked? GPT-5.6 Sol and GPT-6 Astra need code access. Without it the server uses GPT-5.6 Luna and the chat still works.

Why can I not send a message? Check the counter under the box: free accounts get 500 characters per message. Also the send button is disabled at 0 credits. If you see "Chat context is too large. Start a new chat to continue.", press "Clear" and continue in a fresh chat.

Why does the chat say "Too many chat requests"? The limit is 30 messages per 10 minutes with code access and 8 without. Wait a few minutes.

Can I upload my own resource? Yes with Pro or Max: "Upload Resource", ZIP only, 5 MB, 50 text files. Free and Standard users can create a project and paste code into files instead.

How do I undo what Syntax changed? Open "Change History", find the turn and click "Revert changes". Syntax can also create and restore snapshots on request.

Do context documents get shipped with my resource? No. Files under .context/ are excluded from "Download Resource" and from deploys.

My credits went negative, is that a bug? No. A reply is billed after it finishes, so a message can take the balance a little below zero (down to -10, or -50 with code access). You need to add credits before the next message.

Languages

Since 2026-09-20 this part of the site is available in English, French, German, Spanish, Portuguese, Polish and Dutch. It follows the browser's language on a first visit, and the language menu (the icon beside the profile in /app, or in the top navigation on public pages) changes it. Nothing about how it works, what it costs or what a plan includes differs by language; only the words do. File names, format names (GLB, YDR, YTD), product and model names, code, and some error lines that come from the server stay in English. Details and known gaps: Languages and translation.

Topics codesyntaxfivemluanuiresourcesaimodelscredits

Something missing? Ask on Discord