# Postes

> Run a poste (a job-role agent such as the Auditeur de pièces) on one dossier and get a structured verdict, RAS or a list of findings, each tied to a piece.

A **poste** is an agent configured as a job description rather than as a conversation partner: a bounded task, run on a precise input, answering with a precise output. It is what a firm gives to "the person who rereads the file before it leaves". Two postes ship with Subsidia, built for accounting, legal and notarial practices; a firm can also define its own ("maison" postes) in the console.

The defining rule of a poste is that it **signals, it does not conclude**. It reports what is missing or contradictory and names the piece each finding comes from; a member of the firm decides and signs. That makes it safe to put in a pipeline: it never gives advice, never rewrites, never contacts anyone, and never invents a piece, an amount or a date.

## Why a separate endpoint

A poste is stored as an agent, but [Agent chat](https://dev.subsidia.protypa.fr/docs/agent-chat.md) refuses it: free conversation breaks the rules that make a poste reliable (chat encourages answering from general knowledge when documents are silent, exactly what a poste must never do). The only way in is `POST /v1/postes/:key/run`, which runs the poste against **one dossier** and returns a fixed JSON contract. It is the same contract the `agent.run` node of [Reflex](https://dev.subsidia.protypa.fr/docs/reflex.md) reads, so a flow and an external caller can never disagree about what a poste said.

To wire it into an external pipeline (n8n, LangChain, a cron job, a document-management system), call this endpoint when a piece arrives and branch on `status`.

## Available postes

| Key | Name | What it does | Woken by |
| --- | --- | --- | --- |
| `auditeur-pieces` | Auditeur de pièces | Compares the pieces present in a dossier with what that kind of dossier should contain (using the control list set for it) and reports what is **missing** and what **contradicts itself** from one piece to another: a date, an amount or a name that differs. | A piece arriving in a watched dossier. |
| `relecteur-avant-envoi` | Relecteur avant envoi | Takes a draft letter and the pieces of the dossier, lists every figure, date, amount and name of the draft, and reports those that **do not appear** in the pieces. A figure found in the pieces is not a finding. It never rewrites the draft or comments on tone. | A draft produced before it is sent. |
| `<agent id>` | A "maison" poste | A poste your firm created in the console. Its key is the agent id (see [`GET /v1/agents`](https://dev.subsidia.protypa.fr/docs/agents.md): postes have empty `personality` and `decisionStyle`). | Defined by the firm. |

> **INFO: Install before you call**
> A shipped poste only exists in your workspace once someone has installed it from **Light, Les postes** in the Subsidia app. The API does not install postes. Calling the key of a poste that is not installed is a `404` that tells you which one to install.

## Inputs

- **`collectionId`** (required): the dossier, a Cortex collection of the workspace ([Knowledge sources](https://dev.subsidia.protypa.fr/docs/knowledge-sources.md)), that the poste reads. The poste only sees pieces of that dossier. If the poste has its own dossier scope in the console, the run is limited to the overlap: the scope can narrow what is read, never widen it. The usual access rules of the workspace apply on top.
- **`message`** (optional): a custom task instruction. Without it, the poste runs its standard pass ("carry out your mission on this dossier"). Use it to point at a piece ("check the draft saved as lettre-client-v3") or to narrow the pass; you cannot change the output format.

What a run does **not** use: memory (a poste neither reads nor writes any, so two runs on the same dossier do not influence each other), tools, and colleague consultation. Only the dossier's documents and the poste's own rules.

## Output

| Field | Type | Meaning |
| --- | --- | --- |
| `status` | `"RAS"` or `"signalements"` | `RAS` ("rien à signaler"): nothing to report. `signalements`: at least one finding. |
| `signalements` | object[] | The findings; empty when `status` is `RAS`. Each has `constat` (one sentence: what is missing or contradictory) and `piece` (the piece or draft concerned). |
| `posteId`, `posteName` | string | The agent that ran. |
| `sessionId` | string | The session that holds this run (see [Sessions](https://dev.subsidia.protypa.fr/docs/sessions.md)). |
| `knowledgeUsed` | object[] | The passages the poste read to reach its verdict, with `sourceName`, `page` and `chunkPreview`, for traceability. |
| `tokenUsage` | object | `promptTokens`, `completionTokens`, `totalTokens` of the run. |

> **WARNING: RAS means the poste found nothing, not that the dossier is right**
> A poste checks what its rules and control list tell it to check, against the pieces it can read. `RAS` is a statement about that pass. A poste with no control list can see what is there but not what is missing, which is why the list is edited in the console and is business knowledge, not technical setup. Findings are written in French because the shipped postes are French-language. If the model returns something that is not the expected JSON, the run still succeeds: `status` is `signalements` with a single finding whose `constat` is the raw text and whose `piece` is empty, so a malformed answer is never silently read as `RAS`.

## Authentication, scopes and billing

Use a developer API key ([Authentication](https://dev.subsidia.protypa.fr/docs/authentication.md)). The postes route belongs to no capability scope: a **legacy** key (no capability scope) can call it; a **restricted** key (one that carries a capability scope such as `engine`) is refused with `403 scope_denied`; and `readonly` keys are refused because a run writes a session. Create a dedicated unrestricted key for the pipeline that runs postes. The route is limited to 30 calls per minute.

A run counts against the workspace allowance of questions, like a chat turn. An exhausted allowance is a `402` before the model runs.

## Endpoint

### POST /v1/postes/:key/run

Run an installed poste on one dossier.

Executes the poste on the dossier and returns the structured verdict. The call is synchronous: it returns when the poste has read the dossier and answered, which takes seconds with a cloud model and can take minutes on a small local machine. Set a client timeout of five minutes or more.

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

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key` | `string` | yes |  | A shipped poste key (`auditeur-pieces`, `relecteur-avant-envoi`) or the id of a "maison" poste. |

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `collectionId` | `string` | yes |  | The dossier (Cortex collection) to run the poste against. |
| `message` | `string` | no |  | Replaces the default task instruction. Omit for the standard checklist pass. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/postes/auditeur-pieces/run \
  --max-time 600 \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"collectionId": "'"$COLLECTION_ID"'"}'
```

_TypeScript_

```typescript
type Finding = { constat: string; piece: string }
type PosteRun = { status: 'RAS' | 'signalements'; signalements: Finding[]; posteName: string; sessionId: string }

async function runPoste(key: string, collectionId: string, message?: string): Promise<PosteRun> {
  const res = await fetch('https://api.subsidia.protypa.fr/v1/postes/' + key + '/run', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ collectionId, message }),
    signal: AbortSignal.timeout(10 * 60 * 1000),
  })
  if (!res.ok) throw new Error(res.status + ' ' + (await res.text()))
  return res.json()
}

const run = await runPoste('auditeur-pieces', process.env.COLLECTION_ID!)
if (run.status === 'RAS') {
  console.log(run.posteName + ': rien à signaler')
} else {
  for (const f of run.signalements) console.log('- ' + f.piece + ': ' + f.constat)
  process.exitCode = 1 // hold the dossier for a human
}
```

_Python_

```python
import os, requests


def run_poste(key, collection_id, message=None):
    body = {"collectionId": collection_id}
    if message:
        body["message"] = message
    res = requests.post(
        "https://api.subsidia.protypa.fr/v1/postes/" + key + "/run",
        headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
        json=body,
        timeout=600,
    )
    res.raise_for_status()
    return res.json()


run = run_poste("auditeur-pieces", os.environ["COLLECTION_ID"])
if run["status"] == "RAS":
    print(run["posteName"], ": rien à signaler")
else:
    for f in run["signalements"]:
        print("-", f["piece"] + ":", f["constat"])
```

#### Responses

**200**: The verdict. With nothing to report, `status` is `RAS` and `signalements` is an empty array.

```json
{
  "posteId": "cm2kf3r6k000lqz0fy8a2c5dp",
  "posteName": "Auditeur de pièces",
  "status": "signalements",
  "signalements": [
    { "constat": "Le diagnostic amiante, attendu pour un compromis de vente, ne figure pas au dossier.", "piece": "Compromis de vente.pdf" },
    { "constat": "Le prix de vente diffère entre le compromis (412 000 EUR) et le projet d'acte (421 000 EUR).", "piece": "Projet d'acte.docx" }
  ],
  "sessionId": "cm2kf5b2m000mqz0fd3v7j9tq",
  "knowledgeUsed": [
    { "ref": 1, "sourceId": "cm2kf0c9h000nqz0fu6x4s1ge", "sourceName": "Compromis de vente.pdf", "section": null, "page": 3, "chunkPreview": "Prix : quatre cent douze mille euros ...", "similarity": 0.81, "chunkId": "ck_2a9e61b7" }
  ],
  "tokenUsage": { "promptTokens": 5120, "completionTokens": 240, "totalTokens": 5360 }
}
```

**400**: `collectionId` is missing.

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

**402**: The workspace has no questions left. Nothing was consumed.

**404**: The poste is not installed in this workspace (shipped keys) or does not exist (agent id).

```json
{ "error": "Poste non installé : installez « Auditeur de pièces » depuis Light → Les postes avant de l'appeler." }
```

**422**: The key is the id of an ordinary agent, not a poste.

```json
{ "error": "« Claire » n'est pas un poste — utilisez /v1/agents/cm2k8x1ab0001qz0f7h3d9t4e/chat." }
```

**500**: The model call failed.

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 |  | `collectionId` missing. |
| 402 | `API_KEY_BUDGET_EXCEEDED` | The key reached its own monthly question ceiling. |
| 403 | `scope_denied` | The key is restricted by capability scopes: the postes route is outside every scope. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |
| 404 |  | Poste not installed, or no such "maison" poste. |
| 422 |  | The id given is not a poste. |
| 429 | `rate_limited` | More than 30 calls per minute, or the key's own limit. Honour `retry-after`. |

## Wiring a poste into a pipeline

1. **Trigger on arrival**

   Call the endpoint when a piece lands in the dossier (a webhook of your document system, a folder watcher, a scheduled sweep). Pass the dossier's `collectionId`.

2. **Branch on status**

   `RAS` ends the flow quietly. `signalements` creates a task or a notification for a person, with the `piece` and the `constat` of each finding. Do not auto-act on findings: the poste signals, a human decides.

3. **Keep the trace**

   Store `sessionId` with the dossier. [`GET /v1/agents/:id/sessions/:sid`](https://dev.subsidia.protypa.fr/docs/sessions.md) replays the run, and `knowledgeUsed` shows which passages the verdict rested on.

> **TIP: Need a no-model check instead?**
> To check a text you already have (a letter, a note produced elsewhere) claim by claim against the dossier with deterministic verdicts and no model call, use [Le Vérificateur](https://dev.subsidia.protypa.fr/docs/verify.md). Postes read a dossier and report gaps; the Vérificateur grades a given text.
