# Workflows Exported as JSON and committed. The n8n instance is not the source of truth. ## Deploying a change **n8n does not re-register an active workflow when you PATCH it.** The running webhook keeps executing the definition it was activated with, so an edit appears to have no effect — and the failure it keeps producing is the *old* one, which is a very good way to spend an afternoon debugging something already fixed. Deactivate, apply, reactivate, in that order: ``` POST /rest/workflows//deactivate {"versionId": ""} PATCH /rest/workflows/ {"nodes":…, "connections":…, "versionId": ""} POST /rest/workflows//activate {"versionId": ""} ``` `versionId` changes at every step, so re-read it each time. And note `PATCH {"active": true}` returns 200 and leaves the workflow inactive, silently: activation has its own endpoint. ## approvisionnement-check-availability Working. `POST /webhook/check-availability` with the end user's token in `X-Subject-Token`: ``` "Peut-on promettre S10_1949 ?" → {"answer":"Yes — 1952 Alpine Renault 1300 (S10_1949) can be promised: 7305 in stock, above the reorder threshold of 50.","escalate":false} "Peut-on promettre la Vespa ?" → {"answer":"No product reference in the question…","escalate":true} ``` It exchanges the token itself, using the agent's client secret from the pod environment so the committed JSON holds no secret, then talks MCP in two calls: `initialize` to obtain the session id from a **response header**, then `tools/call`. No `notifications/initialized` is needed. MCP rather than the REST route, which would have been one call instead of two: the per-tool ACL by consumer group lives on the MCP route only. Going through REST would give an autonomous agent broader access than a conversational one, which is exactly backwards. The product code is read with a regex, not a model. This agent's critical path is rules; a question naming no product escalates rather than being guessed at. The regex is loose after the underscore on purpose — a reference that looks valid but does not exist must reach the catalog and come back as "no such product", because answering "you named no product" would be false. ### The one that took longest A product code that does not exist returned an **empty body**, and the reason is worth keeping. MCP does not report a tool failure as a JSON-RPC `error`. It reports it as a perfectly successful **result** carrying `isError: true`, with the reason as text: ``` data: {"jsonrpc":"2.0","result":{"content":[{"type":"text", "text":"HTTP call failed with status 404: {\"detail\":\"Not found.\"}"}], "isError":true},"id":2} ``` A client that only checks `rpc.error` therefore reads a failure as data. This one went on to `JSON.parse` the sentence "HTTP call failed with status 404" as if it were a product, threw, and the webhook answered nothing at all. Two lessons, both now in the node: - **check `result.isError`, not just `error`** — they mean different things: a protocol failure versus a tool that ran and failed; - **guard every `JSON.parse` of a payload you did not construct.** The throw is what turned a wrong answer into no answer, which for an autonomous agent is the worse of the two. Now: ``` "Peut-on promettre S99_NOPE ?" → {"answer":"There is no product S99_NOPE in the catalog.","escalate":true} ``` and through the A2A facade, `escalate: true` becomes a task in state **failed** rather than a completed one — the honest shape for "a human has to look at this". ## approvisionnement-periodic-reorder The periodic half. A schedule trigger at 06:00, deliberately periodic rather than a batch standing in for events: grouping references into a purchase proposal follows a buying cycle, while *detecting* a coming stock-out is reactive and waits for Kafka. Verified end to end on the live catalog: ``` scanned 110 of 110, complete 1 reference below the threshold of 50: S24_2000 1960 BSA Gold Star DBD34 (Highway 66 Mini Classics) on hand 15 → propose ordering 135 escalate: true ``` ### It acts under its own machine identity `client_credentials`, not a mandate. An agent running on its own account acts for itself — nobody asked for the 06:00 run, so naming a person as its principal is a fiction and choosing which person is arbitrary. The service account carries **`read_only`**, and the MCP server maps that to a consumer group at discovery, so `create-product` is not merely unused — it is invisible to this identity: ``` tools/list under the machine identity → get-product, list-products ``` Least privilege enforced by the realm and the gateway rather than by this workflow behaving itself. An earlier revision used an `offline_access` mandate; see CLAUDE.md for why it was withdrawn. ### The proposal has nowhere to go It is computed and escalated, never written, because there is nothing to write it to: the `classicmodels` schema has no purchase order and no supplier entity — `productvendor` is a string on the product — and the catalog's only write tool is `create-product`, which is the Catalogue agent's business. So `catalog:write` was **not** granted to this agent, contrary to the design's scope list. A scope with no reachable target is an authorisation waiting for a use, which is the wrong order. Closing this needs a `reorders` entity in the domain, or a channel to a human — there is no mail server in the cluster either. Same shape as the design's own note that a Livraison agent needs a `shipments` domain first. ### It scans one page, on purpose 200 covers the 110 references, and the node reports `complete: false` and escalates if it ever does not. A proposal set computed from part of the catalog looks exactly like one computed from all of it, so the difference has to be in the output. A catalog that outgrows one page needs a paging loop **and** the read-volume cap this repo already lists as mandatory for autonomous agents.