# Flows — the autonomous agents **Runtime:** n8n · **Mode:** autonomous · Two agents in one repo. | Agent | Client id | Trigger | |---|---|---| | Approvisionnement | `classic-models-agent-approvisionnement` | event-driven, plus a periodic ordering run | | Recouvrement | `classic-models-agent-recouvrement` | weekly, Monday | They share a repo because they are not code: n8n workflow exports, same tool, same owner. Splitting them would buy nothing. ## Why low-code for these two Their work is rules, thresholds and due dates — a sequence of conditions and calls, which a visual editor expresses better than code. The LLM is optional on their critical path, which makes them cheaper and more predictable than the conversational agents. They remain full A2A agents: they publish an Agent Card, accept tasks, answer. ## Approvisionnement | | | |---|---| | MCP | catalog, sales | | Scopes | `catalog:read`, `catalog:write`, `sales:read` | Detect coming stock-outs, propose reorders — **two jobs with two different triggers**, which an earlier revision of this file collapsed into one nightly run. *Detection is reactive.* It depends on something happening: an order placed, a stock level crossing a threshold. Finding that out up to a day later is latency chosen for nothing, and a nightly sweep re-reads the whole catalog to rediscover what the domain already knew at the moment it changed. *Proposing a purchase order is periodic.* Nobody reorders on every unit sold; orders are grouped by supplier lead time and buying cycle. A clock is the right trigger there, and that run is periodic on purpose rather than a batch standing in for events. Worth naming, because it changes what has to be built: **the autonomous mandate is a cost of the trigger, not of the business need.** `offline_access`, a refresh token stored encrypted and an exchange at trigger time are required only because nobody is present at 3am. An agent woken by a domain event runs moments after a human acted. **Exposes** `check_availability` — called by Ventes before promising a delivery date, and by Catalogue before discontinuing a reference. Returns a decision and a date, never the stock table. ## Recouvrement | | | |---|---| | MCP | customers, payments | | Scopes | `customers:read`, `payments:read`, `payments:write` | Weekly: unpaid invoices past 30 days, dunning, escalation. **Exposes** `check_creditworthiness` — called by Ventes before accepting a large order. **Dunning is prepared automatically and sent only after human validation.** Sending a letter to a customer is an outward-facing action; a rule bug would otherwise write to the entire customer base in one run. ## The A2A server gap The available n8n community nodes ([n8n-nodes-a2a](https://github.com/agentic-layer/n8n-nodes-a2a), [n8n-nodes-agent2agent](https://github.com/pjawz/n8n-nodes-agent2agent)) make n8n an A2A **client**: `Discover Agent`, `Send Task`. But both agents here must be **servers** — Ventes calls them. That needs an Agent Card on `/.well-known/agent.json` and a JSON-RPC endpoint with the task lifecycle. n8n webhooks can do it by hand, at which point it stops being no-code. Preferred approach: **Kong in front of n8n**, exactly as it fronts every other resource. Kong serves the Agent Card statically and routes JSON-RPC to the webhook, and token verification lands where it already lives. No new component. Alternative: a small FastA2A adapter, written once for both — see `classic-models-agent-catalogue`, which uses FastA2A already. ## An autonomous run is a machine identity **Superseded, 2026-08-29.** An earlier revision had these agents act under an `offline_access` mandate: a user authorises the agent, their refresh token is stored encrypted, and it is redeemed at trigger time so `sub` stays the person who mandated. That was built and then withdrawn, because it is not true. **An agent running on its own account acts for itself.** Nobody asked for the 06:00 run; naming a person as its principal is a fiction, and choosing which person is arbitrary — the first implementation picked whichever test user was to hand. It also cost more than it was worth. An offline token has no `exp`; it dies only after 30 days unused, is not rotated on use (`revokeRefreshToken: false`), and is revocable only by killing that user's offline session. A permanent bearer credential, standing in for a consent nobody gave. So: `client_credentials`, and the service account carries the realm role the job needs. For the periodic reorder run that is **`read_only`** — so `create-product` is not merely unused, it is **invisible** to that identity. Least privilege enforced by the realm and the gateway rather than by the workflow behaving itself. Verified: `tools/list` under the machine identity returns `get-product` and `list-products`, nothing more. What does *not* change: `azp` still names the agent, the audiences are still per-server, and the MCP server still derives the consumer group from the token's roles. Downstream policy still needs no knowledge of the trigger mode — it reads roles, and a service account has roles like anyone else. **Where a mandate would still be right:** when a person's own authority is genuinely in play — an action they are accountable for, or data scoped to them. Recouvrement's dunning is the candidate, and it is a human-validated step already. The `classic-models-mandate` client is left in the realm, unused, for that case. ## Backlog **Events over Kafka, emitted by `classic-models-api`.** The target for the reactive half. The estate emits no domain event today — the API has no signals and no outbound webhook, and there is no bus in the cluster — which is what made a nightly sweep look like the only option. The emission belongs **in the API**, not in a database CDC and not in a gateway plugin. Only the API knows which change is a business event rather than a row update, and only it holds the identity that made it — `sub` and `azp` from the caller's token, which the event has to carry if a downstream agent is to act under the same authority as the person who triggered it. Two facts about that codebase shape the work: - Every write goes through one `BaseModelViewSet` in `api/v1/classicmodels/views.py`. One hook there covers products, orders, payments and the rest — Django signals scattered across models would be the harder version of the same thing. - **The models are `managed = False`.** The tables are the pre-existing classicmodels schema and Django owns no migrations for them. A transactional outbox means introducing the estate's first managed table beside unmanaged ones — which works, and is worth deciding deliberately rather than discovering. An outbox is the part that matters: emitting from a viewset after `save()` drops the event if the process dies between the two, and an agent that never hears about a stock movement is worse than one that hears late. Until this exists, detection stays unbuilt rather than being approximated by a periodic scan: a batch that pretends to be reactive is worse than an absent feature, because it looks finished. The periodic ordering run does not wait on any of it. ## Two controls that become mandatory here They were deferrable with a human in the loop. They are not, once an agent loops on its own. - **Read volume caps per `(azp, sub)`**, enforced on Kong. An agent reads a thousand records in seconds where a human clicks ten; a scope authorises the read but says nothing about volume. - **Authorisation on tool-call parameters**, not just the tool name. `write` authorises writing, not "this €40k transfer". An autonomous agent hitting a rule that requires approval must **fail cleanly and escalate**, never decide. And any approval screen must be rendered from the structured tool-call parameters — never from a sentence the model produced, or a prompt injection writes what the human validates. ## Before writing workflows Read `classic-models-agent-platform/CLAUDE.md` for the identity model, the endpoints and the known blockers. Deliverables in English. Export workflows as JSON and commit them; do not rely on the n8n instance as the source of truth.