Set the category of one or more transactions to ONE label — the write that clears a categorization gate.
REQUIRED: transaction_ids, the ids of the rows from a read (well_list_uncategorized_window, a transaction list, a query). category — the LABEL, exactly as that read returned it on the row's suggestion, or another label from the closed list this schema carries. The vocabulary is fixed: there is no free-text category and no way to mint one.
**A yes, "valide la proposition" or "ok" after another message since the list or proposal it answers names no row. Do not call this tool for that reply. Read the remaining rows again with the uncategorized-window read so Well shows a fresh list; a pick on that list then saves.**
**`decision` records HOW the category was chosen, and it changes what the row keeps.**
- `accepted_classifier_suggestion` — the user affirmed the label the classifier had already put on the row. The row keeps `category_source: "classifier"` and its confidence score, and the affirmation is stamped as `category_confirmed_at`. Send this ONLY when the label equals the classifier's own stored suggestion.
- `user_choice` — the user picked the label themselves. The row records `category_source: "user"` with no score.
The server verifies an `accepted_classifier_suggestion` claim against the row it is writing and downgrades it to `user_choice` when the stored suggestion is not that label, so the claim can never manufacture classifier provenance. Omitting `decision` is a `user_choice`.
**A row from `well_list_uncategorized_window` never qualifies for the affirmation.** That read returns rows carrying NO category at all, so there is no stored classifier value to affirm and the claim would be downgraded every time. Its `categorySuggestions` are PENDING proposals, not a stored category. Clearing that gate is always a `user_choice`; the affirmation exists for a surface that lists rows the classifier already categorized.
Categorizing a row does NOT move it in or out of the internal-transfer rule — that rule counts payment-means legs and no label affects it. What a category DOES change is exemption matching: an uncategorized row can never be matched by an exemption and always stays in a sum.
**Send `category: null` to set the row back to no category.** Use it when the user asks to remove or empty a transaction's category. The row then has no category, and the user owns that state: neither the classifier nor a counterparty rule fills it again. A null category takes no `decision`. "Uncategorised / suspense" is NOT "no category": it is a real category that books the line to the suspense account. Never send it to remove a category.
The server refuses a null category with the typed reason `TRANSACTION_CATEGORY_BOOKED_IN_LEDGER` when the row's category is already booked in the ledger. Tell the user the category is booked in the ledger, and offer to change it to another category instead.
**One call for every row that takes the same label.** When the person names many rows for one category ("put the 12 Uber rides under Transport"), read the rows, then send ALL their ids in `transaction_ids` on ONE call: never one call per row. Up to 50 ids per call; for more rows, split them into calls of at most 50. Rows that take different labels go in separate calls, one per label. `decision` applies to every row of the call, and the server checks an affirmation row by row.
Each row is saved on its own. The answer gives `written` and `failed` and one entry per row in `results`. When some rows failed, say which rows were not changed: never report them as done. `totals` sums the saved rows, one amount per currency, signed as the bank books them: say each amount without its sign, a whole amount with no decimals (€412, never €412.00), and never add up amounts yourself.
**When every row saved, answer with ONE read-back line**: "Saved: the <written> <the rows, in the person's words>, <total> in total, are under <the category, in the person's words>." For example "Saved: the 12 Uber rides, €412 in total, are under Transport." For one row, name it: "Saved: transaction Figma, €45, 12 Oct: category Software." Add nothing else: no list of the rows, no question.
**The list depends on the workspace.** A personal space files household categories, a company workspace business categories, and both file the shared ones. A label outside the workspace's list writes nothing and returns `refusal_reason` with `allowed_categories`: send a `label` from that list.
**A name that fits more than one label is a question, never a pick.** When the words the person used fit two or more labels of the list (for example "Logiciels" or "software" fits both "Developer & engineering software" and "Business & productivity software"), do not call this tool: ask which one, naming each label that fits in the person's words, and call it once they answer. Never choose the nearer label, and never put two labels on one call.
**`confirm_first`: true when the person's message asks that nothing change before they approve** (for example "ne modifie rien avant mon accord", "show me first", "wait for my OK"). Omit it otherwise. In Well's own sessions the call then waits on Well's confirmation card, even when the person lets Well act alone for this kind of change. In another assistant connected to Well with the person's own sign-in, Well shows no card and a flagged call changes nothing: it returns `awaiting_confirmation: true`. Show the person the transaction and the label, ask for their OK in the conversation, then call again without the flag. Without the flag, your host's own approval of the call is the confirmation. Well checks that the person is signed in and a member of the workspace, and that the label is a category of this workspace. A connection made with an API key cannot change a category.
**This write changes Well's copy of the transaction only.** The bank or tool the row was synced from keeps its own category: no call reaches it. When the person named that bank or tool, say that it stays unchanged.
When the token authorizes one workspace, call this directly — no workspace call is needed first. When it authorizes several, this read will not guess which one you mean: pass `workspace_id` on the call.