> Source: https://wexa.ai/docs/administration/project-mode-api

# Project mode

Read a project's mode with `GET /v1/projects/{projectId}/mode` and set it with `PUT` on the same path. Any project-scoped credential can read; writing needs an administrator role.

**Read and set a project's mode**

**REST API**

```bash
curl -sS -H "Authorization: Bearer $FABRIC_API_KEY" \
  https://fabric.wexa.ai/v1/projects/proj_abc/mode
```

```json
{ "project_id": "proj_abc", "mode": "auto", "ui_label": "Simple" }
```

```bash
curl -sS -X PUT https://fabric.wexa.ai/v1/projects/proj_abc/mode \
  -H "Authorization: Bearer $FABRIC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"manual"}'
```

```json
{ "project_id": "proj_abc", "mode": "manual", "ui_label": "Advanced" }
```

## Parameters

| Parameter | Type | Required | Notes |
|---|---|---|---|
| `projectId` | string, in the path | Yes | Must be the project the credential is scoped to; another project's id is refused (403, `project outside token scope`). |
| `mode` | string, in the `PUT` body | Yes, on `PUT` | `auto` or `simple` stores `auto` (Simple); `manual` or `advanced` stores `manual` (Advanced). Lower case only. Any other value is not rejected: it is stored as `auto` and answered with `200`. |

**Errors**

| Status | Code | Raised when |
| --- | --- | --- |
| 403 | `forbidden` | A `PUT` comes from a caller without an administrator role (`changing project mode requires an admin role`). The scope check comes first. |
| 400 | `invalid_request` | The `PUT` body is not JSON. |

**More details**

```
GET /v1/projects/{projectId}/mode
PUT /v1/projects/{projectId}/mode
```

For when and how to use it, read [Project mode](/docs/administration/project-mode).

### Two states, four names

There are exactly **two** states. Each has two accepted spellings, and the two spellings for a state
are synonyms rather than variants:

| Send | Stored as | Shown in the console as |
|---|---|---|
| `auto` | `auto` | **Simple** |
| `simple` | `auto` | **Simple** |
| `manual` | `manual` | **Advanced** |
| `advanced` | `manual` | **Advanced** |

### `GET /v1/projects/{projectId}/mode`

```bash
curl -sS -H "Authorization: Bearer $FABRIC_API_KEY" \
  https://fabric.wexa.ai/v1/projects/proj_abc/mode
```

```json
{ "project_id": "proj_abc", "mode": "auto", "ui_label": "Simple" }
```

Any project-scoped credential can read it — no admin role required. The `{projectId}` in the path
must be the one the credential is scoped to; another project's id is refused rather than answered:

```json
{ "error": "forbidden", "error_description": "project outside token scope" }
```

The gateway reads the mode through to the platform, the source of truth, with a short cache.
If that is briefly unreachable it falls back to the last value this process saw, or the
configured default — so a tool call never fails just because the mode could not be re-read.

### `PUT /v1/projects/{projectId}/mode`

```bash
curl -sS -X PUT https://fabric.wexa.ai/v1/projects/proj_abc/mode \
  -H "Authorization: Bearer $FABRIC_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"manual"}'
```

```json
{ "project_id": "proj_abc", "mode": "manual", "ui_label": "Advanced" }
```

The response is the same shape as the read, reflecting the new state, and a subsequent `GET`
returned the same thing — the write is applied immediately, not eventually.

Writing requires an **admin role** on the project. A project-scoped member is refused:

```json
{ "error": "forbidden", "error_description": "changing project mode requires an admin role" }
```

The scope check comes first, so another project's id is refused before the role is even considered.

A successful write is recorded in the audit stream as `project.mode.changed`, with the new mode in
the payload — so "who put this project into Simple mode, and when" is answerable from
`GET /v1/audit`.

### The failure that looks like a success

A malformed body, one that is not JSON at all, is a real `400`:

```json
{ "error": "invalid_request", "error_description": "…" }
```

Any other unrecognised value is not an error. It is normalised to `auto` and answered with `200`, as the `Advanced` row of the ledger below shows:

```json
{ "project_id": "proj_abc", "mode": "auto", "ui_label": "Simple" }
```

### A safe write

Because a wrong value is indistinguishable from a right one by status code alone, the write is worth
wrapping in a check:

```bash
want=manual
got=$(curl -sS -X PUT "$FABRIC_URL/v1/projects/$PROJECT/mode" \
        -H "Authorization: Bearer $FABRIC_ADMIN_TOKEN" \
        -H "Content-Type: application/json" \
        -d "{\"mode\":\"$want\"}" | jq -r .mode)

[ "$got" = "$want" ] || { echo "mode is '$got', asked for '$want'" >&2; exit 1; }
```

Note that this compares against the stored spelling. Asking for `advanced` stores `manual`, so
compare against `manual` — or compare `ui_label` against `Advanced`, which is stable across both
spellings.

### Per-route ledger

| Case | Result |
|---|---|
| `GET` on the token's own project | `200`, `auto` / Simple |
| `GET` on another project | `403 project outside token scope` |
| `PUT` `{"mode":"manual"}` | `200`, `manual` / Advanced; `GET` confirmed |
| `PUT` `{"mode":"advanced"}` | `200`, stored as `manual` / Advanced |
| `PUT` `{"mode":"simple"}` | `200`, stored as `auto` / Simple |
| `PUT` `{"mode":"Advanced"}` | `200`, silently stored as `auto` / Simple |
| `PUT` on another project | `403 project outside token scope` |

Where the platform cannot persist the mode, the gateway holds it in its own store instead, so a restart returns the project to Simple.

Every tool call works the same way: [How every tool call works](/docs/sdks-and-apis/how-every-tool-call-works).
