# Agents

> Create, read, update and delete the agents (personas) of a workspace through the API.

An **agent** is a persona that lives in a workspace: a name, a role, a character, a mission and a system prompt. Once it exists you can talk to it ([Agent chat](https://dev.subsidia.protypa.fr/docs/agent-chat.md)), give it memory ([Agent memory](https://dev.subsidia.protypa.fr/docs/agent-memory.md)), link it to your own HTTP tools ([Tools](https://dev.subsidia.protypa.fr/docs/tools.md)), link it to knowledge sources ([Knowledge sources](https://dev.subsidia.protypa.fr/docs/knowledge-sources.md)), put it around a table with other agents ([Conversations](https://dev.subsidia.protypa.fr/docs/conversations.md)), or call it by name from any OpenAI or Anthropic client through the Gateway with the model address `agent:<slug>` ([Model addressing](https://dev.subsidia.protypa.fr/docs/model-addressing.md)).

This page covers the persona itself and the four endpoints that manage it. Everything is scoped to the workspace of your API key: another workspace's agents are indistinguishable from missing ones.

## The persona model

Six text fields are required to create an agent. They are not decoration: each one is injected into the prompt the model sees, in a different place and with a different effect. The API returns the full row, including a few fields that only the console edits.

| Field | Type | Set through the API | What it does |
| --- | --- | --- | --- |
| `id` | string | No, generated | Stable identifier. Use it in every path (`/v1/agents/:id/...`). |
| `name` | string | Required | How the agent is named in answers, in debates and in the roster other agents see when they consult a colleague. Keep it short and unique inside the workspace. |
| `role` | string | Required | A job title in a few words ("Contrôle de gestion"). Shown next to the name in every surface and read by the auto-pick router of [Agent chat](https://dev.subsidia.protypa.fr/docs/agent-chat.md). |
| `personality` | string | Required | The character: tone, temperament, habits of speech. This is what makes an answer sound like the agent and not like a generic assistant. |
| `mission` | string | Required | What the agent is for, in one specific sentence. It is the boundary other agents read before consulting this one and the line the agent quotes when it declines an off-topic question. Also read by the auto-pick router. |
| `decisionStyle` | string | Required | How it reasons and chooses: cautious, contrarian, evidence-first, speed-first. It shapes disagreements in debates. |
| `systemPrompt` | string | Required | Free-form instructions appended to the persona: domain rules, output conventions, things to never do. |
| `isDevilsAdvocate` | boolean | Optional, default `false` | Marks a contrarian persona. Debates always seat one devil's advocate per workspace; agents flagged this way are not drawn as regular debaters (see [Conversations](https://dev.subsidia.protypa.fr/docs/conversations.md)). |
| `expertise`, `outOfScope` | string[] | No, console only | The subjects inside the mission, and the subjects to decline and hand off. Returned as empty arrays for agents created through the API. Postes use them heavily. |
| `collectionScope`, `checklist` | string[] | No, console only | A poste's dossier scope and control list. See [Postes](https://dev.subsidia.protypa.fr/docs/postes.md). |
| `isShared` | boolean | No | Whether the agent's global memories are shared across the workspace. |
| `createdAt` | ISO date | No | Creation time. Lists are ordered by it, oldest first. |

> **INFO: Postes are agents too**
> A poste (a job-role agent such as "Auditeur de pièces") is an agent whose `personality` and `decisionStyle` are empty strings. It appears in `GET /v1/agents`, but free-form chat with it is refused: it runs on a dossier through [`POST /v1/postes/:key/run`](https://dev.subsidia.protypa.fr/docs/postes.md). The API never creates postes; they are installed from the console.

## Design tips

- **One agent, one job.** A precise `mission` beats a long prompt. "Check that every figure is backed by a document of the file" gives the router, the colleagues and the agent itself something to hold on to; "Help with finance" gives them nothing.
- **Put character in `personality` and `decisionStyle`, rules in `systemPrompt`.** Character fields are weighed on every answer; the system prompt is where you write constraints ("never quote a figure without its source", "answer in the language of the question").
- **Do not write output formats into the prompt.** Ask for structure per call with `responseFormat` ([Agent chat](https://dev.subsidia.protypa.fr/docs/agent-chat.md#structured-json-answers)); the prompt stays reusable.
- **Keep facts out of the prompt.** Documents belong in [Knowledge sources](https://dev.subsidia.protypa.fr/docs/knowledge-sources.md) linked to the agent, where they are retrieved, cited and signed into the proof. Per-call data belongs in the `context` array of the chat request.
- **Give the workspace a contrarian.** A debate with only agreeable personas converges in one round. One agent with `decisionStyle` "contrarian" earns its seat.
- **Test with a fixed question set.** Change one field at a time and replay the same three or four questions; persona behaviour moves more than people expect.

## Authentication and scopes

Send the key as `Authorization: Bearer sk_live_...` or `x-api-key: sk_live_...` ([Authentication](https://dev.subsidia.protypa.fr/docs/authentication.md)). A key issued without capability scopes (a legacy key) reaches every route. A **restricted** key, one that carries a capability scope such as `engine`, reaches the agent routes only if it also carries the `agents` scope; otherwise the answer is a `403 scope_denied`. A key with the `readonly` scope can list and read agents but cannot create, change or delete them (`403 read_only_key`).

## Endpoints

### GET /v1/agents

List the agents of the workspace.

Returns every agent of the workspace, oldest first, with all persona fields. The list includes the workspace's devil's advocate once a debate has created it, and any installed poste. Filter on the client side if you only want conversational personas (postes have empty `personality` and `decisionStyle`).

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/agents \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/agents', {
  headers: { Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY },
})
const { agents } = await res.json()
console.log(agents.map((a: any) => a.name + ' (' + a.role + ')'))
```

_Python_

```python
import os, requests

res = requests.get(
    "https://api.subsidia.protypa.fr/v1/agents",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
)
for agent in res.json()["agents"]:
    print(agent["id"], agent["name"], "-", agent["role"])
```

#### Responses

**200**: An object with an `agents` array.

```json
{ "agents": [
{
  "id": "cm2k8x1ab0001qz0f7h3d9t4e",
  "userId": "cm1u0a0000000qz0fowner001",
  "workspaceId": "cm1w0b0000000qz0fworksp01",
  "name": "Claire",
  "role": "Contrôle de gestion",
  "personality": "Precise, calm, allergic to round numbers nobody can source.",
  "mission": "Check that every figure in a client file is backed by a document of that file.",
  "expertise": [],
  "outOfScope": [],
  "decisionStyle": "Evidence first: never concludes without a cited piece.",
  "collectionScope": [],
  "checklist": [],
  "systemPrompt": "You are Claire, a management controller. Answer in the language of the question...",
  "isDevilsAdvocate": false,
  "isShared": false,
  "createdAt": "2026-10-09T08:15:42.118Z"
}
] }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 401 |  | No key, invalid, revoked or expired key. |
| 403 | `scope_denied` | A restricted key without the `agents` scope. |

### POST /v1/agents

Create an agent.

Creates a persona in the workspace of the key. The six text fields are all required and must be non-empty. The agent is usable immediately: chat with it, link tools, link knowledge sources.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | yes |  | Display name of the agent. |
| `role` | `string` | yes |  | Job title in a few words. |
| `personality` | `string` | yes |  | Tone and temperament. |
| `mission` | `string` | yes |  | What the agent is for, in one sentence. |
| `decisionStyle` | `string` | yes |  | How the agent reasons and decides. |
| `systemPrompt` | `string` | yes |  | Additional instructions and rules. |
| `isDevilsAdvocate` | `boolean` | no | `false` | Mark the persona as a contrarian. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/agents \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Claire",
    "role": "Contrôle de gestion",
    "personality": "Precise, calm, allergic to round numbers nobody can source.",
    "mission": "Check that every figure in a client file is backed by a document of that file.",
    "decisionStyle": "Evidence first: never concludes without a cited piece.",
    "systemPrompt": "You are Claire, a management controller. Answer in the language of the question."
  }'
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/agents', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Claire',
    role: 'Contrôle de gestion',
    personality: 'Precise, calm, allergic to round numbers nobody can source.',
    mission: 'Check that every figure in a client file is backed by a document of that file.',
    decisionStyle: 'Evidence first: never concludes without a cited piece.',
    systemPrompt: 'You are Claire, a management controller. Answer in the language of the question.',
  }),
})
if (res.status !== 201) throw new Error(await res.text())
const { agent } = await res.json()
console.log(agent.id)
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/agents",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={
        "name": "Claire",
        "role": "Contrôle de gestion",
        "personality": "Precise, calm, allergic to round numbers nobody can source.",
        "mission": "Check that every figure in a client file is backed by a document of that file.",
        "decisionStyle": "Evidence first: never concludes without a cited piece.",
        "systemPrompt": "You are Claire, a management controller. Answer in the language of the question.",
    },
)
res.raise_for_status()
agent_id = res.json()["agent"]["id"]
```

#### Responses

**201**: The created agent.

```json
{ "agent": {
  "id": "cm2k8x1ab0001qz0f7h3d9t4e",
  "userId": "cm1u0a0000000qz0fowner001",
  "workspaceId": "cm1w0b0000000qz0fworksp01",
  "name": "Claire",
  "role": "Contrôle de gestion",
  "personality": "Precise, calm, allergic to round numbers nobody can source.",
  "mission": "Check that every figure in a client file is backed by a document of that file.",
  "expertise": [],
  "outOfScope": [],
  "decisionStyle": "Evidence first: never concludes without a cited piece.",
  "collectionScope": [],
  "checklist": [],
  "systemPrompt": "You are Claire, a management controller. Answer in the language of the question...",
  "isDevilsAdvocate": false,
  "isShared": false,
  "createdAt": "2026-10-09T08:15:42.118Z"
} }
```

**400**: A required field is missing or empty. The message names it.

```json
{ "error": "mission is required" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 |  | A required field is missing or an empty string. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |
| 403 | `scope_denied` | A restricted key without the `agents` scope. |

### PUT /v1/agents/:id

Update an agent.

Partial update despite the verb: send only the fields you want to change. Omitted fields (or fields sent as `null`) keep their value. Unlike creation, update does not validate emptiness: an empty string is stored as is, so do not send one unless you mean it. In particular, emptying both `personality` and `decisionStyle` turns the agent into a poste, which can no longer be chatted with.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `name` | `string` | no |  | New display name. |
| `role` | `string` | no |  | New role. |
| `personality` | `string` | no |  | New personality. |
| `mission` | `string` | no |  | New mission. |
| `decisionStyle` | `string` | no |  | New decision style. |
| `systemPrompt` | `string` | no |  | New system prompt. |
| `isDevilsAdvocate` | `boolean` | no |  | Toggle the contrarian flag. |

#### Request examples

_curl_

```bash
curl -X PUT https://api.subsidia.protypa.fr/v1/agents/cm2k8x1ab0001qz0f7h3d9t4e \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mission": "Check every figure of a client file against its documents, and say which one is missing."}'
```

_TypeScript_

```typescript
await fetch('https://api.subsidia.protypa.fr/v1/agents/' + agentId, {
  method: 'PUT',
  headers: {
    Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ mission: 'Check every figure of a client file against its documents, and say which one is missing.' }),
})
```

_Python_

```python
import os, requests

requests.put(
    "https://api.subsidia.protypa.fr/v1/agents/" + agent_id,
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={"mission": "Check every figure of a client file against its documents, and say which one is missing."},
).raise_for_status()
```

#### Responses

**200**: The updated agent.

```json
{ "agent": {
  "id": "cm2k8x1ab0001qz0f7h3d9t4e",
  "userId": "cm1u0a0000000qz0fowner001",
  "workspaceId": "cm1w0b0000000qz0fworksp01",
  "name": "Claire",
  "role": "Contrôle de gestion",
  "personality": "Precise, calm, allergic to round numbers nobody can source.",
  "mission": "Check that every figure in a client file is backed by a document of that file.",
  "expertise": [],
  "outOfScope": [],
  "decisionStyle": "Evidence first: never concludes without a cited piece.",
  "collectionScope": [],
  "checklist": [],
  "systemPrompt": "You are Claire, a management controller. Answer in the language of the question...",
  "isDevilsAdvocate": false,
  "isShared": false,
  "createdAt": "2026-10-09T08:15:42.118Z"
} }
```

**404**: No agent with this id in the workspace.

```json
{ "error": "Agent not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | Unknown id, or the agent belongs to another workspace. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |

### DELETE /v1/agents/:id

Delete an agent and its memories.

Deletes the persona and every memory attached to it. This cannot be undone. Chat sessions that the agent took part in are kept (they are workspace conversations), but the agent can no longer answer in them.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |

#### Request examples

_curl_

```bash
curl -X DELETE https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_Python_

```python
import os, requests

res = requests.delete(
    "https://api.subsidia.protypa.fr/v1/agents/" + agent_id,
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
)
assert res.status_code == 204
```

#### Responses

**204**: Deleted. No body.

**404**: No agent with this id in the workspace.

```json
{ "error": "Agent not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | Unknown id, or the agent belongs to another workspace. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |

## What to do next

- **[Talk to the agent](https://dev.subsidia.protypa.fr/docs/agent-chat.md)**: One-to-one chat, streaming, structured answers, citations.
- **[Give it memory](https://dev.subsidia.protypa.fr/docs/agent-memory.md)**: Read what it remembers and seed facts it should know.
- **[Connect your systems](https://dev.subsidia.protypa.fr/docs/tools.md)**: Register an HTTP endpoint as a tool the agent can call.
- **[Use it from any client](https://dev.subsidia.protypa.fr/docs/model-addressing.md)**: Call it as agent:<slug> through the Gateway.
