# Create a Job (/docs/guides/create-a-job)

To create a job in ESX Legacy, add one row to the `jobs` table and one row per rank to the `job_grades` table, then reload the jobs with `/refreshjobs` (or restart the server). From a script you can do the same at runtime with `ESX.CreateJob`. A job only becomes a "company" with a bank account and boss menu when you also register it with `esx_society`.

## Overview [#overview]

| What | Where |
| --- | --- |
| Job definition | `jobs` table (`name`, `label`, `type`, `whitelisted`) |
| Ranks and salaries | `job_grades` table, one row per grade |
| In-memory cache | `ESX.Jobs`, filled by `ESX.RefreshJobs()` at startup |
| Assign a job | `/setjob` command or `xPlayer.setJob()` |
| Company money and boss menu | `esx_society` + `esx_addonaccount` (optional) |

ESX loads all jobs once when the database is ready. A job that has no rows in `job_grades` is skipped with a warning, and a player whose saved job or grade does not exist is moved to `unemployed` grade `0` when they load.

## How ESX stores jobs [#how-esx-stores-jobs]

The `jobs` table holds one row per job:

| Column | Type | Description |
| --- | --- | --- |
| `name` | `varchar(50)` | Internal job name, primary key. Used in code, for example `police`. |
| `label` | `varchar(50)` | Display name, for example `LSPD`. |
| `type` | `varchar(50)` | Free-form category, default `civ`. The default data uses `civ`, `leo`, `ems` and `mechanic`. `ESX.GetJobs(jobType)` can filter by it. |
| `whitelisted` | `tinyint(1)` | `1` hides the job from `esx_joblisting` (the job center). Default `0`. |

The `job_grades` table holds the ranks:

| Column | Type | Description |
| --- | --- | --- |
| `id` | `int` | Auto-increment primary key. Leave it out of your inserts. |
| `job_name` | `varchar(50)` | Must match `jobs.name`. |
| `grade` | `int` | Rank number, starting at `0`. |
| `name` | `varchar(50)` | Internal grade name, for example `boss`. Available as `xPlayer.job.grade_name`. |
| `label` | `varchar(50)` | Display name of the rank. |
| `salary` | `int` | Paycheck amount per interval. |
| `skin_male` / `skin_female` | `longtext` | JSON uniform data. `NOT NULL`, so insert `'{}'` if you have no uniform. |

See [Database schema](/docs/guides/database-schema) for every table ESX uses.

## Create the job with SQL [#create-the-job-with-sql]

This is the most common way and works on any ESX Legacy server.

<Steps>
<Step>
### Insert the job and its grades [#insert-the-job-and-its-grades]

Run this in HeidiSQL, phpMyAdmin or your MySQL client against the ESX database. The example creates a `bahamas` nightclub job with four ranks.

```sql title="bahamas.sql"
INSERT INTO `jobs` (`name`, `label`, `type`, `whitelisted`) VALUES
  ('bahamas', 'Bahama Mamas', 'civ', 1);

INSERT INTO `job_grades` (`job_name`, `grade`, `name`, `label`, `salary`, `skin_male`, `skin_female`) VALUES
  ('bahamas', 0, 'recruit',  'Recruit',  20,  '{}', '{}'),
  ('bahamas', 1, 'barman',   'Barman',   40,  '{}', '{}'),
  ('bahamas', 2, 'manager',  'Manager',  60,  '{}', '{}'),
  ('bahamas', 3, 'boss',     'Owner',    100, '{}', '{}');
```

Keep `name` lowercase without spaces. `jobs.name` is `varchar(50)`, but `users.job` is only `varchar(20)`, so job names longer than 20 characters cannot be saved on the player.
</Step>
<Step>
### Reload the jobs [#reload-the-jobs]

ESX keeps jobs in memory, so new rows are not visible until you reload them. In the server console (or in game as an admin) run:

```bash
refreshjobs
```

`/refreshjobs` calls `ESX.RefreshJobs()`, which reloads both tables and fires the `esx:jobsRefreshed` server event. A server restart does the same.
</Step>
<Step>
### Assign the job to a player [#assign-the-job-to-a-player]

```bash
setjob 1 bahamas 0
```

The arguments are `[playerId] [job] [grade]`. In game you can use `me` as the player ID: `/setjob me bahamas 3`. ESX checks the job and grade with `ESX.DoesJobExist` and rejects unknown combinations. Who can run `/setjob` is controlled by `Config.CommandPermissions` (owner and admin by default), see [Admin permissions](/docs/guides/admin-permissions).
</Step>
<Step>
### Check the result [#check-the-result]

The `/job` command prints the player's job, grade label and duty status. It runs on the server, so the output appears in the **server console**, not in the player's F8 console. To check from a script, read `xPlayer.getJob()` on the server or `ESX.PlayerData.job` on the client.
</Step>
</Steps>

## Create a job from a script with ESX.CreateJob [#create-a-job-from-a-script-with-esxcreatejob]

If your resource ships its own job, create it at runtime instead of asking server owners to run SQL. `ESX.CreateJob` inserts the job and its grades in one transaction and adds them to `ESX.Jobs` immediately, so no `/refreshjobs` is needed.

```lua
ESX.CreateJob(name, label, grades, jobType)
```

| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | Internal job name. Required. |
| `label` | `string` | Display name. Required. |
| `grades` | `table` | List of grades. Each entry has `grade`, `name`, `label`, `salary` and optional `skin_male` / `skin_female` tables. Required. |
| `jobType` | `string?` | Job type. Defaults to `civ` when it is not a string. |

**Returns:** `boolean`, `true` when the rows were inserted.

```lua title="server/main.lua"
ESX = exports['es_extended']:getSharedObject()

CreateThread(function()
    if ESX.DoesJobExist('bahamas', 0) then
        return
    end

    local ok = ESX.CreateJob('bahamas', 'Bahama Mamas', {
        { grade = 0, name = 'recruit', label = 'Recruit', salary = 20 },
        { grade = 1, name = 'barman',  label = 'Barman',  salary = 40 },
        { grade = 2, name = 'manager', label = 'Manager', salary = 60 },
        { grade = 3, name = 'boss',    label = 'Owner',   salary = 100 },
    }, 'civ')

    if not ok then
        print('[bahamas] could not create the job, see the es_extended log above')
    end
end)
```

Behavior worth knowing:

- If the job already exists, only grades that do not exist yet are inserted. The existing label and type are kept.
- If the job and all grades already exist, it prints an error and returns `false`. Guard the call with `ESX.DoesJobExist` as above to keep the console clean.
- On success it fires the server event `esx:jobCreated` with `(name, job)`.
- `ESX.DoesJobExist` and `ESX.GetJobs` wait until jobs are loaded, so call them from a thread, not at the top level of a file.

### Reload a single job [#reload-a-single-job]

After changing a salary or label directly in the database, `ESX.RefreshJob(jobName)` reloads just that job and updates every online player who has it (`xPlayer.refreshJob()`). If the job no longer exists or has no grades, online players with that job are set to `unemployed`.

```lua title="server/main.lua"
ESX.RefreshJob('bahamas')
```

## Change a player's job in code [#change-a-players-job-in-code]

```lua title="server/main.lua"
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.setJob('bahamas', 1)          -- on duty by default (Config.DefaultJobDuty)
xPlayer.setJob('bahamas', 1, false)   -- explicitly off duty
```

`setJob` triggers the server event `esx:setJob` with `(source, job, lastJob)` and the client event `esx:setJob` with `(job, lastJob)`. It also writes the job to the player's state bag (`Player(source).state.job`). React to job changes on the client like this:

```lua title="client/main.lua"
RegisterNetEvent('esx:setJob', function(job, lastJob)
    if job.name == 'bahamas' then
        print(('Now working as %s (%s)'):format(job.grade_label, job.label))
    end
end)
```

The job table contains `name`, `label`, `type`, `onDuty`, `grade`, `grade_name`, `grade_label`, `grade_salary`, `skin_male` and `skin_female`. Full method list: [xPlayer](/docs/es_extended/server/xplayer).

## Add a society (company account and boss menu) [#add-a-society-company-account-and-boss-menu]

A society gives the job a shared bank account, employee management (hire, fire, promote) and optionally money washing. It needs [esx_society](/docs/addons/esx_society) and [esx_addonaccount](/docs/addons/esx_addonaccount), started **before** your job resource.

<Steps>
<Step>
### Create the shared account [#create-the-shared-account]

```sql title="bahamas_society.sql"
INSERT INTO `addon_account` (`name`, `label`, `shared`) VALUES
  ('society_bahamas', 'Bahama Mamas', 1);
```

You do not need to insert into `addon_account_data`: esx_addonaccount creates the missing row for shared accounts (with `0` money) when it starts. Official jobs also add rows to `datastore` and `addon_inventory` with the same name; you only need those if your script uses a society stash or datastore.
</Step>
<Step>
### Register the society on the server [#register-the-society-on-the-server]

```lua title="server/main.lua"
TriggerEvent('esx_society:registerSociety', 'bahamas', 'Bahama Mamas',
    'society_bahamas', 'society_bahamas', 'society_bahamas', { type = 'private' })
```

The arguments are `name, label, account, datastore, inventory, data`. `name` must equal the job name. The same function is exported: `exports.esx_society:registerSociety(...)`.
</Step>
<Step>
### Open the boss menu from the client [#open-the-boss-menu-from-the-client]

```lua title="client/main.lua"
TriggerEvent('esx_society:openBossMenu', 'bahamas', function()
    ESX.CloseContext()
end, { wash = false })
```

Only grades whose `name` is listed in esx_society's `Config.BossGrades` can use the menu. The default is `['boss'] = true`, which is why the top grade above is named `boss`.
</Step>
</Steps>

With `Config.EnableSocietyPayouts = true` in es_extended, salaries are paid from the society account instead of being created out of thin air. See [Money and accounts](/docs/guides/money-and-accounts).

## Common issues [#common-issues]

<Accordions>
<Accordion title="'Ignoring job ... due to no job grades found'">
The job row exists but `job_grades` has no rows with that `job_name`. Check spelling and case; `job_name` must match `jobs.name` exactly.
</Accordion>
<Accordion title="/setjob says the job is invalid">
Either the grade number does not exist for that job or ESX has not reloaded jobs yet. Run `refreshjobs` and try again.
</Accordion>
<Accordion title="Players lose their job after a restart">
On load, ESX validates the saved `users.job` and `users.job_grade`. If you renamed or deleted a job or grade, those players are reset to `unemployed` grade `0`. A missing `unemployed` grade 0 is replaced by a built-in fallback, but keep it in the database.
</Accordion>
<Accordion title="The boss menu does not open or shows no money">
esx_society must be started before your job resource, the society must be registered with the same name as the job, the `society_<job>` addon account must exist with `shared = 1`, and the player's grade name must be in `Config.BossGrades`.
</Accordion>
</Accordions>

## Related [#related]

- [xPlayer reference](/docs/es_extended/server/xplayer)
- [esx_society](/docs/addons/esx_society)
- [Money and accounts](/docs/guides/money-and-accounts)
- [Database schema](/docs/guides/database-schema)
- [Admin permissions](/docs/guides/admin-permissions)
