### Zoho People — a login that covers a third of the commands, records that live in forms, and three generations of the same API

Four things cause most failures here: the default consent asks for **6 scopes** while the 447 commands span roughly **16**, so two thirds of them fail on a fresh login; there are no "modules" — every record lives in a **form** and is addressed by that form's link name; leave, attendance, time tracking and files each ship in **two or three generations under different group names**; and `--start`/`--limit` map to five different query parameters depending on the command.

#### The login is the first thing that breaks

`zone login people` requests exactly six scopes — `employee.ALL`, `forms.ALL`, `leave.ALL`, `attendance.ALL`, `timetracker.ALL`, `dashboard.ALL` (the prefix is UPPER-CASE `ZOHOPEOPLE`, not `ZohoPeople`). Those cover **155 of the 447 commands**. The other 292 need scope families the default consent never asks for:

| Groups | Scope family their doc pages name |
|---|---|
| `course*`, `learner`, `batch*`, `module`, `content`, `link`, `session`, `online-test`, `offline-test`, `assignment`, `trainer`, `room`, `category`, `lms`, `learning-plan` — the LMS, 151 commands | `ZOHOPEOPLE.training.ALL` |
| `compensation` (23) | `ZOHOPEOPLE.compensation.ALL` |
| `v3-survey` (23) | `ZOHOPEOPLE.survey.ALL` |
| `file` (9) and `v3-file` (15) | `ZOHOPEOPLE.files.ALL` |
| `v3-competency`, `v3-kra`, `v3-skill`, `v3-review-question` (32) | `ZOHOPEOPLE.performance.*` |
| `v3-variable`, `v3-variable-group` (10) | `ZOHOPEOPLE.variables.*` |
| `org`, `function`, `v3-orgstructure`, `v3-hr-process` (8) | `.organization.READ`, `.automation.ALL`, `.orgstructure.*`, `.hrprocess.*` |
| `case` (9), `onboarding` (3), `announcement` (9) | their doc pages name none — they need a wider consent to test |

Exit 4 on a command in that table is a missing scope, not a permission problem inside People. A human has to widen the consent: `zone login people --scopes <csv>` replaces the list entirely, so include the defaults you still need alongside the new family.

`zone people employee list --limit 1` is the registry's own auth probe — the cheapest way to tell a live session from a scope gap.

#### There are no modules — everything is a form

```bash
zone people form list                          # every form and its link name
zone people form fields <formLinkName>         # the field components of one form
zone people view list                          # saved views; zone people view byform <formLinkName>
zone people record count <formLinkName>
```

Employees, departments and designations are just forms. `zone people employee list` and `employee get` are shortcuts hard-wired onto the `employee` form; for anything else use the generic `record` group with the form's link name.

```bash
zone people record list employee --start 1 --limit 200
zone people record list <formLinkName> --search-column EMPLOYEEID --search-value 42
zone people record byview <viewName> --start 1 --limit 100
zone people record get <formLinkName> --record-id <id>
zone people record get-section <formLinkName> --record-id <id>     # same record, grouped by section
zone people record related <formLinkName> --parent-module <form> --id <recordId> --lookupfield-name <field>
```

Reads take the form **link** name. Writes go through a different path shape with a `json/` segment baked in, and carry the whole record as a single form field:

```bash
zone people record add employee --form 'inputData={"EmployeeID":"1042","Email":"a@b.com"}'
zone people record update employee --form recordId=<id> --form 'inputData={"Designation":"Lead"}'
zone people record delete <formLinkName> --record-id <id>
```

`inputData` is a JSON **string inside a urlencoded body** — quote it as one shell argument. `record delete` is a POST and needs `forms.ALL`: there is no `forms.DELETE` scope to grant instead.

#### Three generations, and the group name tells you which

| Subsystem | classic | v2 | v3 |
|---|---|---|---|
| Leave | `leave types`, `leave records`, `leave apply`, `leave add-balance` | the rest of the `leave` group — `records-v2`, `cancel`, `compoff-*`, `report-*`, `holidays`, `customize-balance` | `v3-leave`, `v3-grant`, `v3-compoff`, `v3-holiday` |
| Attendance | `attendance` (26) | — | `v3-attendance`, `v3-shift` |
| Time tracking | `timetracker` (37) | — | `v3-timetracker` (11) |
| Files | `file` (9) | — | `v3-file` (15) |

The `leave` group is itself mixed: four classic `/leave/*` commands, everything else on `/v2/leavetracker/*`. Pick a generation deliberately — a request raised through `v3-leave add` is read back with `v3-leave get <recordId>`, on a different path from `leave cancel <recordId>`.

**Logging hours is classic-only.** `v3-timetracker` covers clients, projects and jobs; it has no time-log, timer or timesheet commands. Hours go through `timetracker addlog`, `editlog`, `logs`, `deletelog`, `timer-start`, `timer-pause`, `timesheet-create`, `timesheet-approve`.

#### Paging: two flags, five parameters

| Commands | `--start` / `--limit` are sent as |
|---|---|
| `record list`, `employee list`, `timetracker *` | `sIndex` + `limit` |
| `record byview` | `sIndex` + **`rec_limit`** |
| `file list`, `v3-file list` | `start` + `limit` |
| `leave records-v2`, `leave compoff-list`, `leave report-*`, `v3-survey` | `startIndex` + `limit` |
| every other `v3-*` group | **`--offset`** + `limit` |
| `announcement list` / `filter` · the `case` reads | **`--start-idx`** · **`--index`**, both required |

A read that refuses to run before sending anything is almost always a required filter: `--index` on `case requested/mine/open/all/unassigned`, `--start-idx` on announcements, `--from`/`--to` on `leave records-v2` and `leave compoff-list`, `--user` on `timetracker logs` and `timesheet-list`, `--from-date`/`--to-date` on `v3-leave list` and `timetracker schedule-list`, `--mode` on `compensation salary`. zone checks these client-side and exits 2.

#### Which employee id a call wants

`--record-id` (record, employee, case, attendance regularization/permission/OD) · `--erecno` (attendance, timetracker; also a positional on `compensation employee-salary <erecno>` and `leave customize-balance <erecno>`) · `--emp-id` / `--email` (classic attendance) · `--user` (classic timetracker) · `--employee` (v2 leave reports, v3-file) · `--employee-zoho-ids`, `--employee-id`, `--employee-email-id`, `--employee-biometric-mapper-id` (the `v3-*` groups). They are not interchangeable; take the one the command's own `--help` offers.

#### Writes come in three body styles

| Style | Where | How |
|---|---|---|
| urlencoded form | `record add/update`, `leave apply`, `attendance checkin`/`bulkimport`, all classic `timetracker`, `announcement`, `file` | `--form k=v`, repeatable |
| payload in the **query string** | the whole LMS surface | `--course-data`, `--module-data`, `--assignment-data`, `--online-test-data`, `--batch-data` … a JSON string in a required query param |
| JSON body | every `v3-*` group, `compensation` writes, `leave compoff-*` | `--data '<json>'` or `--data @file.json` |

Two spellings to watch: `batch-scheduler create` takes **`--batch-sheduler-data`** (Zoho's typo, preserved so the request works) while `batch-scheduler update` takes `--batch-scheduler-data`. Zoho also documents two writes as GET and zone follows: `attendance permission-apply`, and `file folder-save`, which adds or renames a folder.

Most date-bearing commands offer `--date-format` — send it rather than trusting the portal default; `attendance checkin` is documented with `dd/MM/yyyy HH:mm:ss` and `timetracker addlog` with a `yyyy-MM-dd` work date.

#### What does not come back

No recycle bin is exposed here. `record delete`, `file delete`, `announcement delete` (which removes the announcement's whole history), `v3-attendance delete-bulk` (every entry for one employee on one day) and the `v3-timetracker` bulk deletes take ids and remove immediately. `leave cancel` and `v3-leave cancel` change a status and are the recoverable move; `v3-leave delete` is not.

#### Errors → what to do

| What you see | Meaning | Do |
|---|---|---|
| exit 4 on an LMS, compensation, survey, files or performance command | the default consent never asked for that scope family | human re-runs `zone login people --scopes <csv>` with the family added |
| exit 2, "Missing required option" | Zoho demands a filter and zone caught it before the request | add the flag named in the message — see the paging table |
| a group name you expected is missing (`department`, `designation`) | People has no modules | `zone people form list`, then `zone people record list <formLinkName>` |
| HTTP 400 on `record add` / `record update` | `inputData` is not valid JSON, or a key is not a field on that form | check with `zone people form fields <formLinkName>` |
| a v2 leave call rejected while a v3 one works | the two generations are different endpoints on different paths | stay inside one generation for a workflow |
| exit 3 | not signed in | human runs `zone login people` |

> Group and command names, paths, HTTP verbs, positional arguments, required flags, the query parameter each flag maps to, the six scopes `zone login people` requests and the 155/292 split come from the installed specs and zone's registry, so they are exact. The scope family named against each group, the payload shapes (`inputData`, the `*-data` query params) and the date formats follow Zoho's People documentation as recorded in those specs and were **not** re-verified live — this account has no People session. Run `zone login people` and they can be confirmed against a real portal.
