Files
classic-models-agent-flows/workflows/README.md
T
Stéphane Tailland 06c102fb72 feat: the periodic reorder proposal, and what it cannot do
The periodic half of Approvisionnement: a 06:00 schedule that reads the catalog
and proposes purchase quantities for anything below the reorder threshold.
Verified on live data — 110 of 110 references scanned, S24_2000 at 15 on hand,
135 proposed.

Three things it deliberately does not do, each recorded rather than worked
around:

It halts without a mandate. No user session exists at 06:00, and the
offline_access mandate the design calls for does not exist yet, so the first
node stops before any token exchange and before reading anything. An agent
without an identity does nothing rather than inventing an authority.

It writes nothing. The schema has no purchase order and no supplier entity, and
the catalog's only write tool is create-product, which belongs to another agent.
So catalog:write was not granted, contrary to the design's scope list: a scope
with no reachable target is an authorisation waiting for a use.

It scans one page and says so. complete: false escalates, because a proposal set
computed from part of the catalog looks exactly like one computed from all of
it.

Also: check_availability now states its basis. Deducting quantities committed on
open orders would need the sales API to be filterable by product code — it is
not, so the honest alternative was paging 2996 rows per question.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 17:33:54 +02:00

5.8 KiB

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/<id>/deactivate   {"versionId": "<current>"}
PATCH /rest/workflows/<id>              {"nodes":…, "connections":…, "versionId": "<current>"}
POST  /rest/workflows/<id>/activate     {"versionId": "<current>"}

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 stops without a mandate, and that is the point

No user session exists at 06:00, so authority has to be pre-established. The design calls for an offline_access mandate — the authorising user's refresh token, stored encrypted, exchanged at trigger time, so sub stays the person who mandated and azp says which agent acted.

That does not exist yet. The first node looks for APPROVISIONNEMENT_MANDATE_TOKEN and, finding none, halts before any exchange and before reading anything. Verified: the run completes, Exchange token never executes, nothing is read. An agent without an identity does nothing rather than inventing an authority.

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.