<!-- fivemad.com, Markdown source of https://fivemad.com/docs/txadmin/writing-recipes -->
> Source: https://fivemad.com/docs/txadmin/writing-recipes
> Section: txAdmin
> Tags: txadmin, recipe, yaml, deployer, ox_lib, oxmysql, pma-voice, template
> Last updated: 2026-09-22
> Site map for assistants: https://fivemad.com/llms.txt

# Writing your own txAdmin recipe

Build a txAdmin recipe step by step: an example that installs oxmysql, ox_lib, ox_target, pma-voice and a starter resource, creates a table and writes server.cfg.

A recipe is the best way to make your server reproducible. Your dev server, your test server and a fresh live server can all come from the same file, and new developers on your team get a working setup in two minutes. This page builds a complete example recipe and explains each decision. The task reference is in [Recipes explained](/docs/txadmin/recipes).

> [!IMPORTANT]
> The recipe below is an **example** written for these docs. The resource URLs are real (they are the same ones the official Qbox and ESX recipes use), but pin versions you have tested before you rely on it.

## What we'll build

A lean, framework free base for a custom game mode:

- the default Cfx resources (mapmanager, spawnmanager, basic-gamemode...),
- **oxmysql** (database), **ox_lib** (UI and utilities), **ox_target** (third eye interactions),
- **pma-voice** (proximity voice) and **bob74_ipl** (loads GTA's online interiors),
- one starter resource of our own, created by the recipe,
- a database with one table,
- a `server.cfg` with everything wired up.

## Plan the folder layout

```text
resources/
├─ [cfx-default]/    from citizenfx/cfx-server-data
├─ [ox]/             oxmysql, ox_lib, ox_target
├─ [standalone]/     pma-voice, bob74_ipl
└─ [local]/          my_starter (our own)
server.cfg
```

## The recipe

```yaml title="my-base.yaml"
$engine: 3
$onesync: on
name: My Base
version: 1.0.0
author: YourName
description: |
  A lean base with oxmysql, ox_lib, ox_target, pma-voice and a starter resource.
  No framework. Example recipe from the fivemad docs.

variables:
  dbName: null   # let txAdmin create the database with a random name

tasks:
  # 1. Database first: the most likely step to fail
  - action: connect_database

  - action: query_database
    query: |
      CREATE TABLE IF NOT EXISTS `player_stats` (
        `license` VARCHAR(60) NOT NULL PRIMARY KEY,
        `playtime` INT UNSIGNED NOT NULL DEFAULT 0,
        `last_seen` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

  # 2. Default Cfx resources, minus the old chat (the artifact has a built in one)
  - action: download_github
    src: https://github.com/citizenfx/cfx-server-data
    ref: master
    subpath: resources
    dest: ./resources/[cfx-default]

  - action: remove_path
    path: ./resources/[cfx-default]/[gameplay]/chat

  # 3. Overextended libraries from their release zips
  - action: download_file
    url: https://github.com/overextended/oxmysql/releases/latest/download/oxmysql.zip
    path: ./tmp/oxmysql.zip
  - action: unzip
    src: ./tmp/oxmysql.zip
    dest: ./resources/[ox]

  - action: download_file
    url: https://github.com/overextended/ox_lib/releases/latest/download/ox_lib.zip
    path: ./tmp/ox_lib.zip
  - action: unzip
    src: ./tmp/ox_lib.zip
    dest: ./resources/[ox]

  - action: download_file
    url: https://github.com/overextended/ox_target/releases/latest/download/ox_target.zip
    path: ./tmp/ox_target.zip
  - action: unzip
    src: ./tmp/ox_target.zip
    dest: ./resources/[ox]

  # 4. Standalone resources straight from GitHub
  - action: download_github
    src: https://github.com/AvarianKnight/pma-voice
    dest: ./resources/[standalone]/pma-voice

  - action: download_github
    src: https://github.com/Bob74/bob74_ipl
    dest: ./resources/[standalone]/bob74_ipl

  # 5. Our own starter resource, written by the recipe
  - action: write_file
    file: ./resources/[local]/my_starter/fxmanifest.lua
    data: |
      fx_version 'cerulean'
      game 'gta5'

      name 'my_starter'
      description 'Starter resource created by the My Base recipe'
      version '1.0.0'

      shared_script '@ox_lib/init.lua'
      server_scripts {
          '@oxmysql/lib/MySQL.lua',
          'server.lua',
      }
      client_script 'client.lua'

      dependencies { 'oxmysql', 'ox_lib' }

  - action: write_file
    file: ./resources/[local]/my_starter/server.lua
    data: |
      AddEventHandler('playerJoining', function()
          local src = source
          local license = GetPlayerIdentifierByType(src, 'license')
          if not license then return end
          MySQL.insert('INSERT INTO player_stats (license) VALUES (?) ON DUPLICATE KEY UPDATE last_seen = NOW()', { license })
      end)

  - action: write_file
    file: ./resources/[local]/my_starter/client.lua
    data: |
      RegisterCommand('hello', function()
          lib.notify({ title = 'My Base', description = 'Hello from my_starter!', type = 'success' })
      end, false)

  # 6. server.cfg with placeholders
  - action: write_file
    file: ./server.cfg
    data: |
      ## Generated by the My Base recipe
      {{serverEndpoints}}

      sv_hostname "{{serverName}} | My Base"
      sets sv_projectName "{{serverName}}"
      sets sv_projectDesc "{{recipeDescription}}"
      sets tags "custom, base"
      sets locale "en-US"

      sv_licenseKey "{{svLicense}}"
      sv_maxclients {{maxClients}}
      sv_enforceGameBuild 3751
      set steam_webApiKey "none"
      set resources_useSystemChat true
      sv_endpointPrivacy true

      set mysql_connection_string "{{dbConnectionString}}"

      setr voice_useNativeAudio true
      setr voice_useSendingRangeOnly true

      ensure mapmanager
      ensure chat
      ensure spawnmanager
      ensure basic-gamemode
      ensure hardcap

      ensure oxmysql
      ensure ox_lib
      ensure ox_target
      ensure pma-voice
      ensure bob74_ipl

      ensure [local]

      add_ace group.admin command allow
      add_ace group.admin command.quit deny
      {{addPrincipalsMaster}}

  # 7. Fill in every {{placeholder}} in server.cfg
  - action: replace_string
    mode: all_vars
    file: ./server.cfg

  # 8. Clean up
  - action: remove_path
    path: ./tmp
```

## Why it's built this way

- **Database first.** The official guidelines say to put database tasks at the start, because they fail most often (wrong password, no MariaDB). Better to fail in the first second than after two minutes of downloads.
- **`dbName: null`.** The guidelines require recipes to accept txAdmin's random database name instead of forcing one like `es_extended`. We never hard code a name: the connection string comes from `{{dbConnectionString}}`.
- **Release zips for Overextended.** Their GitHub repos hold source code with a web UI that must be built. The release zip contains the built resource. Always use the release for ox_lib, ox_inventory, ox_target.
- **Placeholders in server.cfg.** The guidelines require `{{maxClients}}`, `{{addPrincipalsMaster}}`, `{{serverEndpoints}}` and `{{svLicense}}`. `{{svLicense}}` is replaced automatically at the end, the others by our `replace_string` with `all_vars`.
- **The chat resource** is removed from `cfx-server-data` because `set resources_useSystemChat true` uses the artifact's built in chat, as the official default recipe does.
- **`$onesync: on`** tells txAdmin to set OneSync on. ox_lib, ox_target and pma-voice all assume it.

## Pin your versions

`releases/latest/download/...` and `download_github` without `ref` always fetch the newest version. That's convenient, but a breaking update will break your next deploy. For a recipe you share or use for production:

```yaml
- action: download_github
  src: https://github.com/AvarianKnight/pma-voice
  ref: 0123456789abcdef0123456789abcdef01234567   # a commit you tested
  dest: ./resources/[standalone]/pma-voice

- action: download_file
  url: https://github.com/overextended/ox_lib/releases/download/v3.30.0/ox_lib.zip   # a tagged release you tested
  path: ./tmp/ox_lib.zip
```

The version numbers above are placeholders: look up the current release on each project's Releases page. The guidelines prefer commit hashes over tags, because tags can be moved.

## Test it

1. Push the YAML to a public GitHub repo (or a gist) and copy the **raw** URL.
2. Start a throwaway txAdmin: a separate `TXHOST_DATA_PATH` and `TXHOST_TXA_PORT` so you don't touch your main one.
3. Choose **Remote URL Template**, paste the URL, deploy.
4. Check: the server starts with no red errors, `/hello` shows a notification in game, `player_stats` gets a row when you join.

Or paste the YAML into **Custom Template** to test without hosting it.

## Guidelines if you want your recipe listed

The [txAdmin-recipes README](https://github.com/citizenfx/txAdmin-recipes) lists the rules for recipes in the Popular list. The main ones:

- Start from the CFX Default recipe structure.
- It must "just work": no editing files before the first start.
- Only open source or sharable resources. No leaked content.
- OneSync compatible, `$onesync: on` if supported.
- Admins in the `admin` ACE group must be recognised as admins by your framework.
- NUI that opens on <kbd>Tab</kbd> must check `IsNuiFocused()` so it doesn't steal focus from the txAdmin menu.
- txAdmin menu actions (noclip, god mode, teleport) must not trigger your anticheat, and txAdmin's Heal should revive downed players (listen to `txAdmin:events:playerHealed`).
- Good onboarding: a loading screen that explains the framework, plenty of comments in `server.cfg`.
- Use the same game build as the CFX Default recipes.
