Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.5 KiB
Flows — the autonomous agents
Runtime: n8n · Mode: autonomous · Two agents in one repo.
| Agent | Client id | Trigger |
|---|---|---|
| the supply agent | classic-models-agent-supply |
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.
Supply
| 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,
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.
Events over Kafka — built, and one rule about them
Built 2026-08-31. classic-models-api emits domain events through a
transactional outbox; a relay publishes them; supply-stock-watch
consumes product.stock_changed and is the reactive half this repo had been
waiting on. The contract is classic-models-api/docs/DOMAIN_EVENTS.md — read it
before writing a consumer.
An event carries no actor, and that is deliberate.
An earlier draft of this file said the opposite — that the event had to carry
sub and azp from the caller's token so a downstream agent could act under
the same authority. The user overruled it:
"Nous gérons ici des événements, pas des 'commandes'. L'agent s'exécute avec sa propre identité machine, on se fout de savoir qui est à l'origine de l'émission de l'événement."
The reasoning, kept because it generalises: an event is a fact, not a
command. A consumer acts under its own identity. An actor field would have had
no consumer and one obvious misuse — anyone able to write to the topic could name
any actor and have an agent act as them. A signed token is proof; a JSON field
is not. Causality travels as traceparent, in the system that exists for it.
The emission still belongs in the API rather than in a database CDC or a gateway plugin: only the API knows which change is a business event rather than a row update.
Two controls that become mandatory here
Both built 2026-08-31, before the first agent looped unattended.
- Read volume caps per
(azp, sub)— a scope authorises a read and says nothing about volume; an agent reads a thousand records where a human clicks ten. - Authorisation on tool-call parameters, not just the tool name.
writeauthorises writing, not "this €40k transfer".
The rules that stay true whatever is built next: 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.
The Kafka credential is created by hand
kafka-classic-models in n8n holds the SCRAM password of the
classic-models-agent-supply KafkaUser and the Strimzi cluster CA.
It is not in this repo and must not be: n8n encrypts stored credentials with
N8N_ENCRYPTION_KEY, and the committed workflow JSON references it by id only.
To recreate it after a rebuild:
kubectl -n kafka get secret classic-models-agent-supply \
-o jsonpath='{.data.password}' | base64 -d
kubectl -n kafka get secret kafka-cluster-ca-cert \
-o jsonpath='{.data.ca\.crt}' | base64 -d
then POST to /rest/credentials with type kafka, ssl: true, the CA in the
ca field, saslMechanism: scram-sha-512, brokers
kafka-kafka-bootstrap.kafka.svc.cluster.local:9093. The workflow's
credentials.kafka.id then needs the new id.
The KafkaUser itself lives in infra-kafka/kafka-users.yaml.