Process · avancé — un ticket qui finit en remboursement
Le cas. Un client demande un remboursement dans un ticket. L'agent lit le ticket et la facture, propose le remboursement autorisé et prépare la réponse. Une personne du support confirme : le remboursement part, puis la réponse, qui cite son numéro.
Ce que l'exemple montre. Plusieurs outils (support, paiements), des lectures qui informent l'agent, une validation par l'outil de paiement, deux écritures exécutées dans l'ordre par un seul submit, la seconde lisant le résultat de la première.
examples/actuelle/process-advanced.yaml
# Process · advanced — a support ticket ends in a refund and a reply.
# Several tools (helpdesk, payments, email). Reads inform the agent; two writes are filed by ONE
# submit, in order, the reply carrying the refund's id; a reason and a confirmation are required.
station:
id: support-remboursement
name: Support · remboursements
global:
language: fr
currency: EUR
locale: fr-FR
domain:
domain_type: saas
entity_name: action
entity_name_plural: actions
agent:
tone: professional
name: Support
persona: "un agent de support qui prépare le geste commercial et la réponse au client"
rules:
- "Toujours lire le ticket et la facture avant de proposer un remboursement."
- "Ne jamais proposer un montant supérieur à celui de la facture."
emoji: false
portals:
web:
widget:
origins: [https://support.example.fr]
apis:
- name: helpdesk
openapi_url: https://helpdesk.example.fr/openapi.json
base_url: https://helpdesk.example.fr
operations:
- {id: getTicket, access: read}
- {id: draftReply, access: read}
- {id: sendReply, access: write, reversible: false}
auth: {type: api_key, key_name: Authorization, key_location: header, key_env: HELPDESK_KEY}
- name: paiements
openapi_url: https://payments.example.fr/openapi.json
base_url: https://payments.example.fr
operations:
- {id: getInvoice, access: read}
- {id: proposeRefund, access: read}
- {id: checkRefundPolicy, access: read}
- {id: createRefund, access: write, reversible: false}
- {id: deleteCustomer, access: write, exposed: false} # listed, never reachable
auth: {type: api_key, key_name: Authorization, key_location: header, key_env: PAYMENTS_KEY}
skills:
- name: lire-ticket
invoke: {method: getTicket}
- name: verifier-facture
invoke: {method: getInvoice}
- name: rembourser
auto_trigger: ticket-remboursement
cognitive:
objective: "proposer le remboursement autorisé pour la facture du ticket"
diagnose: [le motif du client, la ligne de facture concernée]
invoke: {method: proposeRefund} # the refundable lines of the invoice
kinds: {refund: {}}
present:
fields:
name: {from: label, type: text, role: title}
price: {from: amount, type: price, role: billed}
select:
max_items: 1
guards:
- form:
reason: {type: text, required: true, label: "Motif du remboursement"}
- check: checkRefundPolicy # the payment tool says whether this refund is allowed
submit:
- name: pay
mode: request
tool: createRefund
args:
body.line_id: "$item.ref"
body.amount: "$item.price"
body.reason: "$item.fields.reason"
body.idempotency_key: "$action.key"
output: {id: "$output.id"}
render: {component: cards}
- name: repondre
cognitive:
objective: "préparer la réponse au client qui annonce le remboursement"
invoke: {method: draftReply}
kinds: {reply: {}}
select:
max_items: 1
depends_on: rembourser # no reply about a refund that is no longer chosen
on_loss: clear
present:
fields:
name: {from: subject, type: text, role: title}
submit:
- name: send
mode: request
tool: sendReply
args:
body.draft_id: "$item.ref"
body.refund_id: "$submitted[rembourser.pay].id" # filed after the refund; held if it failed
render: {component: cards}
playbooks:
- name: ticket-remboursement
goal: "rembourser le client puis lui répondre"
tasks:
rembourser: {skill: rembourser, required: true, label: "Remboursement"}
repondre: {skill: repondre, required: true, label: "Réponse au client"}
submit_guard:
confirm: true # a person on the support team files both actions
Ce qui se passe
| étape | Core | résultat |
|---|---|---|
| l'agent lit le ticket et la facture | lire-ticket, verifier-facture | le contexte, sans rien écrire |
| propose le remboursement | rembourser → proposeRefund; auto_trigger ouvre le parcours | une carte par ligne remboursable |
| la ligne choisie | motif demandé, puis checkRefundPolicy | réservée, ou refusée avec la raison du paiement |
| la réponse préparée | repondre → draftReply; depends_on: rembourser | deuxième ligne |
| « Valider » + confirmation | createRefund, puis sendReply avec refund_id | deux reçus |
| le remboursement échoue | ADR-028 §3 | la réponse est mise en attente : on n'annonce pas un remboursement qui n'a pas eu lieu |
À retenir — et ce qui manque
Ce cas fonctionne aujourd'hui, mais plusieurs garanties reposent encore sur l'agent ou sur la personne :
- L'ordre : rien n'impose de lire la facture avant de proposer. La consigne le demande ; Core ne le garantit pas.
- Le montant : il vient de l'outil de paiement (
proposeRefund), pas du modèle. Bien. Mais rien ne compare ce montant à la facture lue. - L'approbation : la confirmation du submit est l'approbation. Il n'y a pas de seuil (« au-delà de 50 €, un manager »), ni de personne différente de celle qui prépare.
- L'irréversible :
createRefundetsendReplysont déclaréesreversible: false, et la porte MCP annonce le submit comme destructif. Rien d'autre ne le lit : l'ordre du submit reste celui de la config.deleteCustomerest listéeexposed: false: documentée, jamais joignable. - Le destinataire de la réponse vient du brouillon du support ; rien n'empêche une réponse détournée par un contenu malveillant du ticket.
La version cible de cet exemple ferme chacun de ces points.