### Zoho Analytics — an org header, a CONFIG query param instead of a body, and SQL that runs as an async job

Four things cause most failures here: every call outside `org orgs` needs the **ZANALYTICS-ORGID** header, request parameters ride in a URL-encoded JSON query param named **CONFIG** even on writes (so most write commands have no `--data` at all), running SQL is a **three-request job** rather than a query, and row deletes are keyed by a criteria string rather than by id.

#### The id chain: org → workspace → view

```bash
zone analytics org orgs                              # the only call that needs no org header
zone ctx analytics org_id=<id>                       # injected as ZANALYTICS-ORGID everywhere else
zone analytics workspace list                        # owned + shared
zone analytics view list <workspaceId>               # tables, reports AND dashboards, one list
zone analytics view metadata <workspaceId> <viewId>  # columns and their attributes
zone analytics meta folders <workspaceId>
zone analytics meta datasources <workspaceId>
```

A table, a chart, a pivot and a dashboard are all **views** and all arrive in the same `view list` — there is no per-type endpoint, so filter on the type in the response. Watch the argument asymmetry: `zone analytics view get <viewId>` takes only a view id (`/views/:viewId`), while `view metadata`, `view rename`, `view delete` and the rest take `<workspaceId> <viewId>`. Dashboards do get their own read-only shortcut: `zone analytics dashboard list`.

#### Parameters ride in CONFIG — most writes have no `--data`

```bash
zone analytics row add <workspaceId> <viewId> --config '{"columns":{"Region":"UAE","Amount":"1200"}}'
zone analytics row update <workspaceId> <viewId> --config '{"columns":{"Amount":"1500"},"criteria":"\"Region\"='\''UAE'\''"}'
zone analytics view rename <workspaceId> <viewId> --config '{"viewName":"Q4 pipeline"}'
zone analytics folder create <workspaceId> --config '{"folderName":"Finance"}'
```

`--config` is a JSON object that zone URL-encodes into the `CONFIG` query parameter — on POST, PUT and DELETE as well as GET. Reaching for `--data` on `row add` fails with `error: unknown option '--data'`, because those specs declare no JSON body. Only the import commands (`data import`, and the `bulk import-*` rows) actually take a body.

#### `zone analytics query` is a state machine, not a request

`zone analytics query <workspaceId> "SELECT …"` is the one command that is not a single HTTP call. Analytics executes SQL as an **export job**, and the handler drives all three steps:

1. `GET /bulk/workspaces/<id>/data?CONFIG={sqlQuery,responseFormat}` → returns a `jobId`.
2. `GET /bulk/workspaces/<id>/exportjobs/<jobId>` polled every `--poll` seconds (default 3) until `jobCode` is **1004** (completed). **1005** means the job failed and the command raises. Anything else means still queued or running.
3. `GET /bulk/workspaces/<id>/exportjobs/<jobId>/data` — the payload, CSV by default, parsed into rows.

```bash
zone analytics query <workspaceId> "SELECT \"Region\", SUM(\"Amount\") FROM \"Sales\" GROUP BY \"Region\"" --toon
zone analytics query <workspaceId> "SELECT * FROM \"Sales\"" --format json --out sales.json
```

Things that follow from that shape:

- **Only `SELECT` runs.** zone refuses anything else client-side before a request is made; writes go through `row`/`data`/`bulk`.
- **`--limit` only trims what is printed** — the whole result set is still downloaded, and the output's `info` carries both `count` and `total`. To make the *job* smaller, put `LIMIT` in the SQL.
- **Timing out does not cancel the job.** The default `--timeout` is 120s; on expiry the error names the job id, and you can collect it later with `zone analytics bulk export-download <workspaceId> <jobId> --out rows.csv`. Raise `--timeout` for large extracts rather than re-submitting.
- `--out` writes the raw export body verbatim and skips parsing.
- The three raw steps stay available as `zone analytics bulk export-job-sql`, `bulk export-job` and `bulk export-download` if you want to submit now and collect in a later session.

This is **SQL over the workspace's own tables** — the `FROM` names are view names as they appear in `view list`, quoted with double quotes. It is not CRM's COQL and not Catalyst's ZCQL; module API names from CRM do not exist here unless a CRM connector has synced them in as tables. To persist a query instead of running it ad hoc, use `zone analytics query-table create <workspaceId> --config '{"queryTableName":"…","sqlQuery":"SELECT …"}'`.

#### Exporting a view is *not* the same call

`zone analytics data export <workspaceId> <viewId> --config '{"responseFormat":"json"}' --out view.json` is a synchronous GET and is right for ordinary view data. `zone analytics bulk export-job-view` submits the same thing as an async job, which is what you want for volumes that would time out. Imports mirror it: `zone analytics data import` is synchronous, `bulk import-job-new` / `bulk import-job-existing` are jobs whose progress you read with `zone analytics bulk import-job <workspaceId> <jobId>`. Real file uploads are multipart and the spec notes say to use curl for those.

#### There is no paging

Not one Analytics command in this surface takes a page or per-page flag. Volume is controlled inside `CONFIG` (`criteria`) or inside the SQL (`LIMIT`), never by paging the endpoint. If a list looks short, it is the API's own cap, not a page boundary you can advance.

#### What is destructive

```bash
zone analytics view delete <workspaceId> <viewId> --config '{"withDependents":false}'   # → trash
zone analytics view trash <workspaceId>                                                 # what is in there
zone analytics view restore <workspaceId> <viewId> --config '{"withDependents":true}'
zone analytics view purge <workspaceId> <viewId> --config '{"withDependents":true}'     # permanent
```

- Views go to a **trash** and are recoverable; `view purge` and `workspace delete` are not.
- `zone analytics row delete` takes a **criteria string, not ids** — a criteria that matches more than you meant deletes more than you meant, with no undo. Check the match first with a `SELECT … WHERE` on the same criteria.
- `withDependents` cascades: run `zone analytics view dependents <workspaceId> <viewId>` before deleting a table that reports are built on.
- `zone analytics embed make-public` puts a view on the open internet, and `embed create-private-url` mints a shareable link that bypasses login. Both are publishing actions, not read operations.
- `zone analytics email-schedule trigger` sends a real report email immediately.

Cross-org copies (`workspace copy`, `view copy`, `formula copy`) need a `--dest-org <id>` header flag plus the source workspace's key from `zone analytics workspace secretkey`.

#### Errors → what to do

| What you see | Meaning | Do |
|---|---|---|
| every call fails except `org orgs` | `org_id` is not in ctx, so the ZANALYTICS-ORGID header is absent | `zone ctx analytics org_id=<id>` |
| "mandatory parameter is missing" on a write | you sent a body; Analytics wanted `CONFIG` | move the payload into `--config '<json>'` |
| `error: unknown option '--data'` | that command declares no JSON body | same — use `--config` |
| "Only SELECT statements can be exported." | zone's own guard before any request | writes go through `row`, `data import` or `bulk` |
| export job failed (`jobCode` 1005) | Analytics rejected the SQL | check table and column spelling against `view metadata` |
| job did not finish within the timeout | the job is still running server-side | raise `--timeout`, or collect later with `bulk export-download <workspaceId> <jobId>` |
| fewer rows than expected, no page flag to advance | there is no paging | widen `criteria`, or raise/remove `LIMIT` in the SQL |
| a delete removed the wrong rows | `row delete` matches by criteria | there is no undo — verify the criteria with a `SELECT` first |
| exit 3 | not signed in — Analytics is its own consent | human runs `zone login analytics` |
| exit 4 | the token lacks a `ZohoAnalytics.*` scope family (data / metadata / modeling / share / usermanagement are separate) | human re-logs in that service |

> Command names, groups, paths, HTTP verbs, required flags, the `--config`/`CONFIG` mechanism, the `org_id` → ZANALYTICS-ORGID mapping, and the whole `zone analytics query` job state machine (jobCode 1004/1005, the poll and timeout defaults, `--limit` trimming only the printout) come from the installed specs and from `src/commands/handlers/analytics.js`, so they are exact. Response field names and the `CONFIG` payload keys in the examples follow Zoho's Analytics v2 documentation and were **not** re-verified live — this account has no Analytics session. Run `zone login analytics` and they can be confirmed against a real org.
