Aller au contenu principal
Version: Cible · roadmap

Événements

Un fait n'est pas une action​

questionqui décidegardes, sélection, submitConvoy garantit
un événement« qu'est-ce qui s'est passé ? »personne : c'est arrivénon : un fait ne se refuse pasqu'il est livré : au moins une fois, signé, avec un identifiant pour dédoublonner
une action« qu'est-ce qu'on change, et dans quel outil ? »une personne, ou un agent autorisé par une règleoui : proposée → réservée → exécutéequ'elle est permise, puis exécutée une seule fois

Prévenir le système du tenant n'est pas une action : Convoy ne change rien chez lui, il l'informe, et le tenant décide quoi en faire.

La règle de tri :

  • Le tenant a seulement besoin de savoir (son CRM, son tableau de bord) : c'est une livraison outbound.
  • Quelque chose doit être fait dans un outil (créer l'avoir dans l'ERP, écrire au client) : c'est une action. Elle passe par une skill, ses politiques, une réservation et un submit, même quand c'est un agent qui soumet.

Trois sources, deux consommateurs​

événementd'où il vientoù on le déclare
entrantle webhook d'un outil, ou une autre stationevents.inbound
interneCorenulle part : un catalogue fixe, qu'on cite par son nom
émisla station, à partir d'un événement interne ou entrantevents.emits
consommateurquice qu'il fait
automatisationla stationstart un playbook, remind une personne, notify par un canal Convoy. Jamais une écriture dans un outil, jamais un submit.
outboundun tierslivre le fait à une URL. Pas de condition métier, pas de primitive Convoy.

Une automatisation décide de faire quelque chose avec les primitives de Convoy. Une livraison outbound ne décide rien : elle transporte. Et notify passe par les canaux de Convoy — la boîte d'un rôle, un portail de l'agent, le client de la room. Tout ce qui passe par un outil du tenant est une action.

Les automatisations ont leur propre bloc : elles agissent, alors que events ne fait que déclarer et transporter.

Les événements internes​

Fixés par Core, les mêmes pour toutes les stations. On ne les déclare pas.

familleévénementssujet (of)
roomroom.opened, room.closed—
actionaction.proposed, action.pending, action.reserved, action.refused, action.removed, action.executed, action.held, action.failedla skill de l'action
playbookplaybook.opened, playbook.finalised, playbook.abandoned, playbook.superseded, task.opened, task.doneun playbook
submitsubmit.started, submit.completedun playbook
politiquepolicy.evaluated (seulement quand elle restreint), policy.disputedune politique
budgetbudget.reachedun budget
personnesapproval.requested, approval.granted, approval.denied, approval.expiredune boîte
événementsautomation.fired, event.received, event.rejected, delivery.failedune automatisation, un événement, un outbound

of : le sujet​

Chaque famille n'a qu'un genre de sujet. Une seule clé suffit donc, of, quelle que soit la famille :

on: playbook.finalised                                   # tous les playbooks
on: {event: playbook.finalised, of: ticket-refund} # un seul
on: {event: action.reserved, of: refund} # les actions de la skill refund
on: {event: action.pending, of: refund, older_than: 2h} # toujours en attente 2 h après
on: {event: policy.evaluated, of: fraude}

Le chargement vérifie que of existe dans sa famille : un playbook pour playbook.*, une skill pour action.*.

Le bloc events​

events:
inbound: # les faits REÇUS
- name: zendesk.ticket.created
from: {webhook: /hooks/zendesk, verify: {hmac_sha256: {header: X-Zendesk-Signature, secret_env: ZD_SECRET}}}
id: "$payload.ticket.id" # un webhook rejoué est ignoré
fields:
ticket_id: {from: ticket.id, type: id}
tags: {from: ticket.tags, type: text}
requester: {from: ticket.requester.email, type: text, class: personal}
room: # la room de ce ticket, ouverte si absente
key: "$event.ticket_id"
owner: {role: support} # l'employé du support qui le prend
concerns: "$event.requester" # de qui il s'agit : prévenu, jamais propriétaire

emits: # les faits PUBLIÉS : un contrat stable, des champs choisis
- name: refund.completed
on: {event: playbook.finalised, of: ticket-refund}
fields:
ticket_id: "$room.id"
amount: {value: "$submitted[geste.pay].amount", type: price}
refund_id: "$submitted[geste.pay].id"
# pas l'e-mail du client : personne n'en a besoin en aval

outbound: # la LIVRAISON des faits publiés
- name: crm
on: [refund.completed]
to: https://crm.example.fr/hooks/convoy
audience: external
secret_env: CRM_HOOK_SECRET # chaque livraison est signée
retry: {max: 10, backoff: exponential}

from et to se répondent. on a le même sens partout — dans un emit, un outbound, une automatisation : sur ces événements. Un emit n'est pas attaché à un playbook : il peut partir d'un fait entrant (la cuisine dit « livrée » → order.delivered).

Recevoir : from, verify, id, fields​

clésens
fromun webhook, ou une station : {station: ticket-refund, event: refund.completed}
verifyla signature. Un message qui ne se vérifie pas est event.rejected, jamais lu.
idl'identifiant qui dédoublonne
fieldstypés et classés, comme ceux d'une skill. Un champ non déclaré n'est jamais lu.
roomla room à laquelle le fait se rattache, et son propriétaire si elle est nouvelle

À quelle room se rattache un fait reçu : room​

Un webhook arrive au niveau de la station ; room dit pour qui. Le fait s'écrit alors dans le journal de cette room, les automatisations connaissent $room et son reçu, et notify: customer sait à qui parler.

écritprimitiveexemple
room: {key: "$event.ticket_id", owner: {role: support}}la room, par une clé qu'on lui donne ; ouverte si absente, avec son propriétaireun ticket Zendesk : une room par ticket
room: {returned_by: [take-meal-order.order, group-lunch.order], id: "$event.order_id"}la room où l'un de ces appels a renvoyé cet identifiant (dans son output) : <playbook>.<appel> pour un appel du playbook, <skill>.<appel> pour celui d'une skillla cuisine dit « prête » ; un remboursement échoue tard
(absent)la station seuleun contrat signé dans Lucca : c'est l'automatisation start … for: qui ouvre la room

key sert quand l'objet de l'outil ouvre la room (un ticket). returned_by sert à la suite d'une action déjà exécutée : l'objet créé au submit continue de vivre dans l'outil — une commande préparée puis livrée, un remboursement qui échoue, une société immatriculée, un ordinateur expédié, une enveloppe signée — et le fait revient vers la room d'où l'objet est parti. Comme elle nomme des appels, un id: 42 chez Stripe n'est jamais pris pour l'id: 42 chez Zendesk.

Une room ouverte par un événement a un propriétaire, toujours un user (voir Human In The Loop). La personne dont il s'agit est concernée (concerns) : on peut la prévenir, elle n'agit sur rien.

Un fait qui ne correspond à aucune action est journalisé au niveau de la station ; les automatisations se déclenchent quand même, avec un $room vide.

Les noms​

  1. Dans une station, un nom désigne un seul événement, entrant ou émis. Une station ne reçoit jamais son propre fait émis.
  2. Les préfixes internes sont réservés : room., action., playbook., task., submit., policy., budget., approval., automation., event., delivery..
  3. Entre deux stations, le même nom désigne volontairement le même fait. La station qui reçoit ne le redéclare pas : elle cite le contrat, et le nom et les champs en viennent.
events:                                       # la station Comptabilité
inbound:
- from: {station: ticket-refund, event: refund.completed}

Une convention aide à lire : un fait entrant d'un outil porte le nom de l'outil (zendesk.ticket.created), un fait publié porte un nom métier (refund.completed).

Ce qui sort​

  1. Seuls les faits émis sortent de la station. Les événements internes servent aux automatisations, au journal et à la console. Un fait émis est un contrat stable, nommé par vous : le journal peut évoluer sans casser le système de quelqu'un.
  2. Un fait émis obéit aux classes de données. Un champ confidential ne part pas vers un outbound external. C'est une politique comme les autres : elle apparaît dans le registre, sous Sortie.

Si le CRM ne répond pas, Convoy réessaie, puis la livraison passe en delivery.failed dans la console. Rien n'est annulé : le remboursement a eu lieu, c'est un fait.

De bout en bout​

Process — un ticket devient un remboursement, puis la compta et le CRM suivent​

#ce qui arrivenaturequi porte l'effet
1Zendesk envoie le webhook, signéévénement entrant—
2ouvrir-sur-ticket : le tag billing est làautomatisation : startConvoy ouvre le playbook
3lectures, remboursement de 120 €, approbation du manager dans Teams, réponse au clientactionsle manager soumet : createRefund, puis sendReply
4playbook.finalisedévénement interne—
5refund.completed, trois champs choisisévénement émis—
6le CRM reçoit le fait et l'ajoute à sa timelineoutbound : livraisonle CRM
7la station Comptabilité reçoit le fait et ouvre rapprocher-remboursementévénement entrant, puis automatisationla Comptabilité, sous ses propres règles

« Après le remboursement, créer l'avoir dans l'ERP » n'est pas une livraison : c'est un changement dans un outil. Soit un deuxième appel du même submit (submit: [pay, ledger]), si l'avoir fait partie de la même décision ; soit un playbook de la station Comptabilité, ouvert par le fait — l'étape 7.

Product — une commande, du submit au client​

events:
inbound:
- name: kitchen.order.status_changed
from: {webhook: /hooks/kitchen, verify: {hmac_sha256: {header: X-Kitchen-Signature, secret_env: KITCHEN_SECRET}}}
id: "$payload.order_id + ':' + $payload.status"
fields:
order_id: {from: order_id, type: id}
status: {from: status, type: text} # accepted | ready | out_for_delivery | delivered
room: {returned_by: [take-meal-order.order, group-lunch.order], id: "$event.order_id"} # les deux playbooks passent la commande
emits:
- name: order.delivered
on: kitchen.order.status_changed # un fait entrant, pas un playbook
if: "$event.status == 'delivered'"
fields: {order_id: "$event.order_id", total: {value: "$receipt.total", type: price}}
outbound:
- {name: fidelite, on: [order.delivered], to: https://loyalty.example.fr/hooks,
audience: external, secret_env: LOYALTY_SECRET}

automations:
- name: pret-au-comptoir
on: kitchen.order.status_changed
if: "$event.status == 'ready' and $receipt.kind == 'pickup'"
then: {notify: customer, message: "Votre commande est prête au comptoir."}
#ce qui arrivenature
1le client soumet : placeOrder, puis createDeliveryactions
2la cuisine envoie status: readyévénement entrant, rattaché à la room par l'appel placeOrder du playbook, qui a renvoyé cet identifiant
3pret-au-comptoir prévient le client sur son portailautomatisation : notify, par un canal Convoy
4status: deliveredévénement entrant
5order.deliveredévénement émis, à partir d'un événement entrant
6le programme de fidélité crédite des points chez luioutbound

Si la cuisine rejoue le même webhook, l'identifiant order_id:status le dédoublonne : le client n'est pas prévenu deux fois.

Process — de l'arrivée d'un salarié à la revue des droits​

#ce qui arrivenature
1lucca.contract.signed ; le salaire est classé confidentialévénement entrant
2demarrer-onboarding : arrivée dans 12 joursautomatisation : start
3comptes, ordinateur, e-mail d'accueilactions, réservées par les RH, soumises par l'IT
4onboarding.completed, avec employee_id et start_date, sans le salaireévénement émis. Un salary ajouté par erreur est refusé au chargement.
5la station « Accès et sécurité » ouvre « revue des droits à J+30 »événement entrant d'une station, puis automatisation
6l'annuaire RH du tenant reçoit le faitoutbound

Ce que le chargement vérifie​

écriteffet
un on: qui cite un événement ni interne ni déclaréerreur
un of qui ne nomme rien de sa familleerreur
deux événements du même nom dans une station, ou un préfixe interneerreur
un champ émis dont la classe dépasse l'audience d'un outbounderreur
un fait entrant sans verify ou sans iderreur
une room ouverte par un événement ou une automatisation sans ownererreur : who: owner ne désignerait personne
un emit que personne n'écouteinformation : un consommateur peut venir plus tard
une boucle d'événements dans la stationerreur

Entre stations, aucune config ne voit toute la boucle : chaque événement porte l'identifiant du parcours qui l'a causé, et une automatisation ne se déclenche pas sur un événement causé par son propre parcours.