# Workflows

> Créer, exécuter, publier et surveiller des workflows, décider de leurs approbations et les démarrer depuis des webhooks.

Un workflow est un processus automatisé composé d'étapes : un déclencheur démarre une exécution, et les étapes
qui le suivent appellent des outils, exécutent des assistants, créent des branches, bouclent, envoient des requêtes HTTP, attendent
l'approbation d'une personne et définissent le résultat. L'API des workflows permet à votre application de lister les workflows, de les
exécuter avec une progression en direct, de lire les exécutions passées, de décider des approbations, de modifier et publier des workflows, et
de démarrer des workflows publiés depuis une URL de webhook publique.

Chaque appel s'exécute au nom de l'utilisateur propriétaire de la clé API, avec les permissions,
les organisations et les crédits de cet utilisateur. Les exécutions démarrées via l'API exécutent le **brouillon** du workflow
(la définition actuelle) ; les versions publiées sont exécutées par les plannings, les webhooks et les déclencheurs
d'enregistrements booster.

## Portées et accès

| Portée | Autorise |
|---|---|
| `ayeto.workflow` | Lister et lire les workflows, les exécuter (y compris en streaming), annuler des exécutions, lister et lire les exécutions, le catalogue, la validation, la prévisualisation de planning, l'historique des révisions, l'export, les approbations (lister, compter, lire, décider). |
| `ayeto.workflow.write` | Créer, mettre à jour et supprimer des workflows, supprimer des exécutions, les informations de déploiement (contiennent les secrets des webhooks), publier, activer, régénérer les secrets des webhooks, restaurer une révision, importer, copier, l'assistant builder. |
| `*` | Tout. |

Les portées ne s'incluent pas l'une l'autre : une clé qui doit à la fois lire et modifier des workflows a besoin
des deux. Une clé sans la portée requise reçoit `401` avec `API key is invalid` ; voir
[authentification](authentication.md#portees).

En plus de la portée, chaque workflow vérifie qui peut faire quoi :

| Qui | Peut |
|---|---|
| Propriétaire (le créateur, ou un membre du groupe propriétaire) | Tout. |
| Utilisateur avec qui il est partagé en écriture | Le lire, le modifier, l'exécuter, le publier, l'exporter, le copier et le supprimer. |
| Utilisateur avec qui il est partagé en lecture | Le lire, le valider, voir ses révisions. L'exécuter, l'exporter ou le copier uniquement si le partage accorde la permission `workflow.run`, `workflow.export` ou `workflow.copy`. |

Un workflow qui appartient à une organisation s'exécute dans cette organisation (ses crédits et son
stockage), l'utilisateur doit donc y être membre avec un accès en écriture.

## Concepts

### Définition

La définition d'un workflow est un graphe orienté acyclique : une liste de `nodes` et une liste
d'`edges`.

```json
{
  "nodes": [
    {
      "id": "start",
      "type": "trigger.manual",
      "name": "Start",
      "params": {
        "input_schema": {
          "type": "object",
          "properties": {"customer": {"type": "string"}, "question": {"type": "string"}},
          "required": ["question"]
        }
      },
      "position": {"x": 0, "y": 0}
    },
    {
      "id": "answer",
      "type": "assistant.run",
      "name": "Draft an answer",
      "params": {
        "assistant_id": "3f6c1a2e-8b4d-4e7a-9c1f-2d5e6a7b8c90",
        "prompt": "Answer {{ input.customer }}: {{ input.question }}"
      },
      "position": {"x": 0, "y": 160}
    },
    {
      "id": "result",
      "type": "output",
      "name": "Result",
      "params": {"value": {"answer": "{{ steps.answer.output.text }}"}},
      "position": {"x": 0, "y": 320}
    }
  ],
  "edges": [
    {"source": "start", "target": "answer", "source_handle": null},
    {"source": "answer", "target": "result", "source_handle": null}
  ]
}
```

| Champ | Type | Description |
|---|---|---|
| `nodes[].id` | chaîne | Identifiant du nœud, unique dans le workflow : lettres, chiffres et `_`, au plus 64 caractères, ne commençant pas par un chiffre. Les étapes suivantes font référence à son résultat par `{{ steps.<id>.output }}`. |
| `nodes[].type` | chaîne | Type de nœud, voir [types de nœuds](#types-de-nuds). |
| `nodes[].name` | chaîne | Libellé affiché dans l'éditeur. |
| `nodes[].params` | objet | Paramètres du type de nœud ; leur schéma JSON se trouve dans le [catalogue](#catalogue). |
| `nodes[].position` | objet | `{x, y}` sur le canevas de l'éditeur, ou `null`. Sans effet sur les exécutions. |
| `edges[].source` | chaîne | Nœud dont part l'arête. |
| `edges[].target` | chaîne | Nœud dans lequel entre l'arête. |
| `edges[].source_handle` | chaîne | Sortie du nœud source dont part l'arête : `null` pour la sortie par défaut, une sortie nommée (`true` / `false` d'une condition, un cas d'un switch, `item` / `done` d'une boucle, `approved` / `rejected` / `timeout` d'une approbation), ou `error` pour continuer lorsque l'étape source échoue. |

Une définition comporte au plus 200 nœuds, au moins un déclencheur, aucun cycle et aucune arête entrant dans un
déclencheur. Utilisez [valider](#valider-un-workflow) pour la vérifier.

Les paramètres de type chaîne sont des templates (syntaxe Jinja, évalués dans un bac à sable). Ils peuvent lire :

| Variable | Contenu |
|---|---|
| `input` | L'entrée de l'exécution (la sortie du déclencheur). |
| `steps.<id>.output` | Résultat d'une étape précédente ; également `steps.<id>.success`, `.status`, `.text`, `.error`, `.file_ids`. |
| `workflow` | `{id, name}`. |
| `run` | `{id}`. |
| `now` | Heure de début de l'exécution (ISO 8601, UTC). |
| `loop` | À l'intérieur du corps d'une boucle : `item`, `index`, `count`, `parent`. |

Un paramètre constitué d'une seule `{{ expression }}` conserve le type natif de la valeur (un
objet reste un objet) ; une variable non définie est une erreur de l'étape.

### Types de nœuds

La liste complète, avec le schéma JSON des paramètres de chaque nœud et les outils qu'une étape `tool.call`
peut utiliser, est fournie par le [catalogue](#catalogue). Vue d'ensemble :

| Type | Catégorie | Ce qu'il fait |
|---|---|---|
| `trigger.manual` | déclencheur | Démarre une exécution à la demande (application, API). Le paramètre facultatif `input_schema` décrit l'entrée de l'exécution. |
| `trigger.schedule` | déclencheur | Démarre le workflow publié selon un planning cron. |
| `trigger.webhook` | déclencheur | Démarre le workflow publié lorsque son [URL de webhook](#webhooks) est appelée. |
| `trigger.booster_record` | déclencheur | Démarre le workflow publié lorsqu'un enregistrement de la base de données d'un panneau Booster est créé, mis à jour ou supprimé. |
| `tool.call` | action | Appelle un outil d'IA avec les paramètres donnés. |
| `assistant.run` | action | Exécute une tâche sur un assistant dans une nouvelle conversation. |
| `http.request` | action | Envoie une requête HTTP à une URL publique. |
| `notify` | action | Envoie un e-mail à l'utilisateur au nom duquel l'exécution s'effectue. |
| `file.to_text` | action | Lit des fichiers (documents, images, audio) sous forme de texte. |
| `human.approval` | logique | Attend qu'une personne approuve ou rejette, éventuellement avec un formulaire. |
| `condition` | logique | Continue sur `true` ou `false` selon une expression. |
| `switch` | logique | Continue sur l'une de plusieurs sorties nommées, ou sur `otherwise`. |
| `ai.condition` | logique | Pose une question oui/non à un modèle de classification ; continue sur `true` ou `false`. |
| `ai.choice` | logique | Demande à un modèle de classification laquelle de plusieurs options convient. |
| `loop` | logique | Exécute les étapes situées derrière sa sortie `item` une fois par élément d'une liste, puis continue sur `done`. |
| `transform` | logique | Construit une valeur à partir des résultats précédents. |
| `output` | logique | Définit le résultat de l'exécution. N'a pas d'arêtes sortantes. |

### Brouillon et version publiée

La `definition` enregistrée sur le workflow est le **brouillon**. Chacune de ses modifications est conservée sous forme de
[révision](#revisions). Les exécutions démarrées via l'API (et depuis l'application) exécutent le brouillon.

[Publier](#publier-un-workflow) copie le brouillon dans une nouvelle version immuable (`1`, `2`,
...) et active le workflow. Tant que le workflow est actif, ses déclencheurs automatiques
(planning, webhook, enregistrement booster) démarrent des exécutions de la **version publiée**, au nom de l'utilisateur
qui l'a publiée ou activée en dernier (`run_as`). Modifier le brouillon par la suite ne change pas
ce qu'exécutent les déclencheurs tant que vous ne publiez pas à nouveau.

### Statuts d'exécution

Une exécution enregistre l'entrée, l'état de chaque étape et le résultat. Son `status` :

| Statut | Signification |
|---|---|
| `queued` | Démarrée par un déclencheur, ou exécution en pause dont l'étape en attente a reçu son résultat ; attend un worker en arrière-plan. |
| `running` | En cours d'exécution. |
| `waiting` | En pause : une étape attend quelque chose d'extérieur à l'exécution (l'approbation d'une personne). Les autres branches sont terminées. L'exécution reprend en arrière-plan dès que l'étape reçoit son résultat. |
| `success` | Terminée. |
| `failed` | Une étape a échoué sans arête `error`, une limite a été dépassée, ou l'exécution n'a pas pu démarrer. |
| `cancelled` | Annulée alors qu'elle était `waiting` ou `queued`. |

Les étapes s'exécutent dès que toutes les étapes qui les précèdent sont terminées, de sorte que les branches indépendantes s'exécutent en
parallèle. Une étape dont toutes les arêtes entrantes sont inactives (une branche non empruntée) est `skipped`.
Une étape en échec qui a une arête `error` continue par celle-ci ; sans elle, l'exécution échoue (les étapes
déjà en cours se terminent d'abord).

Les exécutions sont supprimées après une période de rétention (90 jours par défaut), avec les
conversations et les fichiers créés par leurs étapes. Les exécutions en pause sont conservées jusqu'à leur fin.

### Processus d'approbation

Une étape `human.approval` ouvre une approbation et met l'exécution en pause. Les personnes qui peuvent en décider
sont l'utilisateur au nom duquel l'exécution s'effectue et les membres de l'organisation du workflow désignés dans l'étape ;
ils sont notifiés dans l'application et par e-mail. Une approbation affiche un titre, un message et
éventuellement un formulaire (un schéma JSON plat avec des valeurs préremplies). La décision (approuver avec les
données du formulaire, ou rejeter) devient la sortie de l'étape et l'exécution continue sur `approved` ou
`rejected`. Une approbation dont personne ne décide à temps expire : l'exécution continue sur `timeout` lorsque
cette sortie est connectée, sinon l'étape échoue. Voir [approbations](#approbations).

### Fichiers dans l'entrée d'une exécution

Un champ de l'`input_schema` du déclencheur manuel déclaré comme
`{"type": "object", "format": "file"}` contient un fichier. Envoyez-le dans l'entrée de l'exécution soit
directement en base64 :

```json
{
  "invoice": {
    "filename": "invoice-1042.pdf",
    "mime_type": "application/pdf",
    "data": "data:application/pdf;base64,JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGg..."
  }
}
```

soit par l'id d'un fichier que l'utilisateur possède déjà (`"invoice": "7b1e2c3d-..."` ou
`{"file_id": "7b1e2c3d-..."}`). `data` est du base64 brut ou une data URL ; `name` peut être utilisé
à la place de `filename` et `content_type` à la place de `mime_type`. La limite de taille des uploads et
le quota de stockage s'appliquent.

Dans l'exécution, la valeur devient les métadonnées de pièce jointe du fichier, qui sont aussi la forme sous laquelle les étapes
renvoient des fichiers :

```json
{
  "file_id": "7b1e2c3d-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
  "filename": "invoice-1042.pdf",
  "content_type": "application/pdf",
  "file_url": "https://ayeto.ai/api/v1/public/file/read?id=7b1e2c3d-4a5b-4c6d-8e7f-9a0b1c2d3e4f&secret=...",
  "size": 48213
}
```

`file_url` télécharge le fichier. Les fichiers envoyés en base64 appartiennent à l'exécution et sont supprimés avec
elle ; un fichier désigné par son id reste celui de l'utilisateur et n'est pas supprimé avec l'exécution. La même forme base64 est acceptée dans les champs fichier d'un
[formulaire d'approbation](#decider-dune-approbation) et dans le corps des [webhooks](#webhooks).

### Limites

Les valeurs par défaut ci-dessous peuvent varier selon le déploiement.

| Limite | Valeur par défaut |
|---|---|
| Nœuds par workflow | 200 |
| Entrée d'une exécution | 200 000 caractères de JSON |
| Étapes exécutées par exécution (chaque élément de boucle compte) | 500 |
| Durée d'une exécution (les pauses pour approbation ne comptent pas) | 1 800 secondes |
| Étapes exécutées simultanément dans une exécution | 4 |
| Éléments par boucle | 100 |
| Sortie enregistrée d'une étape | 200 000 caractères de JSON (2 000 dans le corps d'une boucle) |
| Exécutions en file d'attente par workflow | 50 |
| Intervalle de planning le plus court | 5 minutes |
| Délai pour décider d'une approbation | 72 heures par défaut, au plus 720 |
| Rétention des exécutions | 90 jours |
| Révisions conservées par workflow | 100 |

## Objet workflow

Le workflow renvoyé par les endpoints de lecture, de création et de mise à jour. Il comporte aussi les
[champs communs](conventions.md#champs-communs) (`id`, `created`, `updated`, `permissions`,
...).

| Champ | Type | Description |
|---|---|---|
| `name` | chaîne | Nom. |
| `description` | chaîne | Description. |
| `organization_id` | UUID | Organisation à laquelle appartient le workflow, `null` pour un workflow personnel. Fixée à la création. |
| `definition` | objet | La [définition](#definition) du brouillon. |
| `sharing` | tableau d'objets | Utilisateurs avec qui il est partagé : `{user_email, write, scopes}`. `scopes` peut accorder aux partages en lecture `{"workflow.run": true, "workflow.export": true, "workflow.copy": true}`. |
| `group_sharing` | tableau d'objets | Groupes avec qui il est partagé : `{group_id, write, scopes}`. |
| `last_run` | objet | Dernière exécution terminée ou en pause : `{run_id, status, finished_at, credits}`, ou `null`. Lecture seule. |
| `published_version` | entier | Dernière version publiée, `null` s'il n'a jamais été publié. Lecture seule. |
| `published_at` | horodatage | Date de la dernière publication. Lecture seule. |
| `active` | booléen | Les déclencheurs automatiques de la version publiée sont activés. Lecture seule ; voir [activer](#activer-ou-desactiver). |
| `run_as` | UUID | Utilisateur au nom duquel s'effectuent les exécutions automatiques. Lecture seule. |

## Lister et lire les workflows

| | |
|---|---|
| Endpoints | `POST /api/v3/workflow/find`<br>`POST /api/v3/workflow/count`<br>`POST /api/v3/workflow/get?entity_id=<workflow id>` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

`find` renvoie les workflows dont l'utilisateur est propriétaire ou qui sont partagés avec lui, sous forme de tableau de
[workflows](#objet-workflow), du plus récent au plus ancien sauf autre tri. `count` renvoie le
nombre de workflows correspondants sous forme d'entier. Les deux acceptent le
[corps de requête](conventions.md#interroger-des-listes) habituel (`filters`, `sort_key`, `sort_order`,
`limit_from`, `limit_to`) ; envoyez `{}` pour tout obtenir. Chaque workflow est renvoyé avec sa
définition complète, paginez donc les longues listes.

`get` renvoie un [workflow](#objet-workflow) ; l'id est un paramètre de requête et le corps
est vide.

Filtres utiles : `["organization_id", "==", "<organization id>"]`,
`["organization_id", "==", null]` (personnels), `["active", "==", true]`,
`["name", "regex", "invoice"]`.

```bash
curl -X POST "https://ayeto.ai/api/v3/workflow/find" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters": [["active", "==", true]], "sort_key": "updated.timestamp", "sort_order": 1, "limit_from": 0, "limit_to": 20}'
```

```bash
curl -X POST "https://ayeto.ai/api/v3/workflow/get?entity_id=5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a" \
  -H "uni-api-key: $AYETO_API_KEY"
```

```json
{
  "id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
  "name": "Customer question",
  "description": "Drafts an answer to a customer question",
  "organization_id": null,
  "definition": {"nodes": ["..."], "edges": ["..."]},
  "sharing": [],
  "group_sharing": [],
  "last_run": {"run_id": "c2a1b0d9-8e7f-4a6b-9c5d-4e3f2a1b0c9d", "status": "success", "finished_at": 1759401234567, "credits": 0.42},
  "published_version": 3,
  "published_at": 1759300000000,
  "active": true,
  "run_as": "0f1e2d3c-4b5a-4968-8776-655443322110",
  "created": {"timestamp": 1759000000000, "user_id": "0f1e2d3c-4b5a-4968-8776-655443322110"},
  "updated": {"timestamp": 1759400000000, "user_id": "0f1e2d3c-4b5a-4968-8776-655443322110"}
}
```

Un workflow qui n'existe pas ou n'est pas visible pour l'utilisateur renvoie `404`.

## Créer un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/create` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `name` | chaîne | Oui | Nom. |
| `description` | chaîne | Non | Description. |
| `organization_id` | UUID | Non | Le créer dans cette organisation ; l'utilisateur doit y avoir un accès en écriture. À omettre pour un workflow personnel. |
| `definition` | objet | Non | La [définition](#definition). Par défaut : un seul déclencheur manuel avec l'id `start`. |
| `sharing` | tableau d'objets | Non | Partages avec des utilisateurs, voir l'[objet workflow](#objet-workflow). |
| `group_sharing` | tableau d'objets | Non | Partages avec des groupes. |

Renvoie le [workflow](#objet-workflow) créé. La définition est enregistrée telle quelle, même
si elle n'est pas valide ; [validez](#valider-un-workflow)-la avant de l'exécuter. La création d'un workflow
enregistre aussi sa première révision et crée l'[assistant builder](#assistant-builder) de l'utilisateur.

```bash
curl -X POST "https://ayeto.ai/api/v3/workflow/create" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Customer question", "description": "Drafts an answer to a customer question"}'
```

## Mettre à jour un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/update` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `id` | UUID | Oui | Le workflow. |
| `name` | chaîne | Non | Nouveau nom. |
| `description` | chaîne | Non | Nouvelle description. |
| `definition` | objet | Non | Nouvelle [définition](#definition) du brouillon ; remplace toute la définition. |
| `sharing` | tableau d'objets | Non | Remplace les partages avec des utilisateurs. |
| `group_sharing` | tableau d'objets | Non | Remplace les partages avec des groupes. |

Seuls les champs que vous envoyez sont modifiés. L'organisation, l'état de publication et la dernière exécution ne peuvent pas
être modifiés ici. Chaque modification de la définition est enregistrée comme [révision](#revisions) ; la
version publiée reste inchangée jusqu'à ce que vous [publiiez](#publier-un-workflow) à nouveau. Renvoie
le [workflow](#objet-workflow) mis à jour.

Pour modifier un nœud, lisez le workflow, modifiez sa `definition` et renvoyez-la. Deux
clients qui mettent à jour le même workflow en même temps écrasent mutuellement leur définition.

## Supprimer un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/delete?entity_id=<workflow id>` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

Supprime le workflow avec toutes ses exécutions (et les conversations et fichiers qu'elles ont créés), ses
révisions, ses versions publiées, ses déclencheurs et ses assistants builder. Renvoie l'id supprimé sous forme de
chaîne JSON.

## Catalogue

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/catalog` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

Renvoie les éléments à partir desquels les workflows peuvent être construits : chaque type de nœud avec le schéma JSON de ses
paramètres, et les outils qu'une étape `tool.call` peut appeler. Pas de corps de requête.

| Champ | Type | Description |
|---|---|---|
| `node_types[].type` | chaîne | Type de nœud, par exemple `http.request`. |
| `node_types[].title` | chaîne | Nom affiché. |
| `node_types[].description` | chaîne | Ce que fait le nœud et à quoi ressemble sa sortie. |
| `node_types[].category` | chaîne | `trigger`, `action` ou `logic`. |
| `node_types[].is_trigger` | booléen | Le nœud démarre des exécutions. |
| `node_types[].terminal` | booléen | Le nœud termine une branche (pas d'arêtes sortantes). |
| `node_types[].handles` | tableau de chaînes | Sorties nommées ; vide signifie une seule sortie par défaut. Chaque nœud qui n'est pas un déclencheur a aussi la sortie `error`. Pour un `switch`, les sorties sont les noms de ses cas plus `otherwise`. |
| `node_types[].params_schema` | objet | Schéma JSON de `params`. |
| `tools[].name` | chaîne | Nom de l'outil pour le paramètre `tool` de `tool.call`. |
| `tools[].display_name` | chaîne | Nom affiché. |
| `tools[].description` | chaîne | Description. |
| `tools[].category` | chaîne | Catégorie. |
| `tools[].icon` | chaîne | Nom de l'icône. |
| `tools[].input_schema` | objet | Schéma JSON des `params` de l'outil. |

## Valider un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/validate` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |

Valide le brouillon pour l'utilisateur appelant (y compris le fait que l'utilisateur puisse utiliser les assistants
et les panneaux Booster auxquels il fait référence).

```json
{
  "valid": false,
  "issues": [
    {"severity": "error", "message": "Node 'answer' has no output 'true' (outputs: default, error)", "node_id": "answer", "edge": {"source": "answer", "target": "result", "source_handle": "true"}},
    {"severity": "warning", "message": "Node 'draft' is not reachable from any trigger and never runs", "node_id": "draft", "edge": null}
  ]
}
```

`valid` vaut `false` lorsqu'il existe au moins un problème de sévérité `error` ; les erreurs bloquent les exécutions
et la publication, les avertissements non.

## Exécuter un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/run` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

Exécute le brouillon au nom de l'utilisateur appelant et répond lorsque l'exécution est terminée ou en pause.

### Requête

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |
| `input` | objet | Non | L'entrée de l'exécution. Pour un déclencheur manuel, elle doit correspondre à son `input_schema` ; les clés listées dans `required` doivent être présentes. Au plus 200 000 caractères en JSON. |
| `trigger_node_id` | chaîne | Non | Déclencheur à partir duquel démarrer. Par défaut : le premier `trigger.manual`, sinon le premier déclencheur de n'importe quel type. |

Démarrer à partir d'un déclencheur de planning, de webhook ou d'enregistrement booster exécute le brouillon manuellement, ce qui est
utile pour les tests : passez l'entrée que le déclencheur produirait, par exemple
`{"body": {...}, "query": {}, "headers": {}}` pour un déclencheur webhook.

L'utilisateur a besoin du droit d'exécuter le workflow (voir [portées et accès](#portees-et-acces)).
Avant le démarrage de l'exécution, le brouillon est [validé](#valider-un-workflow) ; un brouillon invalide est
rejeté avec `422`. Une clé d'entrée obligatoire manquante ne rejette pas la requête : l'exécution est
créée et échoue à son étape de déclencheur (`Missing input: <keys>`).

La requête répond à la fin de l'exécution, ce qui peut prendre plusieurs minutes (jusqu'à la limite de durée d'exécution).
Pour les exécutions longues ou une progression en direct, utilisez le [streaming](#execution-en-streaming). Lorsqu'une étape attend une
approbation, la réponse revient avec `status: "waiting"` ; suivez l'exécution avec
[obtenir l'exécution](#executions).

### Réponse

`200 OK` avec l'[exécution](#objet-execution).

### Exemples

```bash
curl -X POST "https://ayeto.ai/api/v3/workflow/run" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    "input": {"customer": "Jana Novak", "question": "When will order 1042 be delivered?"}
  }'
```

```python
import json
import os
import requests

response = requests.post(
    "https://ayeto.ai/api/v3/workflow/run",
    headers={"uni-api-key": os.environ["AYETO_API_KEY"]},
    json={
        "workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
        "input": {"customer": "Jana Novak", "question": "When will order 1042 be delivered?"},
    },
    timeout=1900,
)
response.raise_for_status()
run = response.json()
if run["status"] == "success":
    print(json.loads(run["output_json"]) if run["output_json"] else None)
else:
    print(run["status"], run["error"])
```

```javascript
const response = await fetch("https://ayeto.ai/api/v3/workflow/run", {
  method: "POST",
  headers: {
    "uni-api-key": process.env.AYETO_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    workflow_id: "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    input: { customer: "Jana Novak", question: "When will order 1042 be delivered?" },
  }),
});
if (!response.ok) throw new Error((await response.json()).detail);
const run = await response.json();
const output = run.output_json ? JSON.parse(run.output_json) : null;
console.log(run.status, output ?? run.error);
```

### Objet exécution

Une exécution comporte les [champs communs](conventions.md#champs-communs), plus :

| Champ | Type | Description |
|---|---|---|
| `workflow_id` | UUID | Le workflow. |
| `organization_id` | UUID | Organisation dans laquelle l'exécution a eu lieu, ou `null`. |
| `version` | entier | Version publiée exécutée ; `null` pour une exécution du brouillon. |
| `trigger_node_id` | chaîne | Déclencheur à partir duquel l'exécution a démarré. |
| `trigger_type` | chaîne | Son type de nœud, par exemple `trigger.manual`, `trigger.webhook`. |
| `trigger_depth` | entier | Nombre d'exécutions déclenchées par des enregistrements booster qui précèdent celle-ci dans une chaîne. |
| `automatic` | booléen | Démarrée par un déclencheur (planning, webhook, enregistrement booster), et non par une personne. |
| `input_json` | chaîne | L'entrée de l'exécution sous forme de texte JSON. |
| `status` | chaîne | Voir [statuts d'exécution](#statuts-dexecution). |
| `started_at` | horodatage | Moment où l'exécution a commencé ; `null` tant qu'elle est en file d'attente. |
| `active_since` | horodatage | Début de l'exécution en cours (après une pause, la reprise). |
| `finished_at` | horodatage | Moment où elle s'est terminée, ou `null`. |
| `steps` | tableau d'[étapes](#objet-etape) | État de chaque étape démarrée ou ignorée, dans l'ordre d'exécution. |
| `output_json` | chaîne | Le résultat (valeur de la dernière étape `output` atteinte) sous forme de texte JSON ; vide s'il n'y en a pas. |
| `error` | chaîne | Raison de l'échec de l'exécution. |
| `credits` | nombre | Crédits dépensés par l'exécution jusqu'à présent. |

`created.user_id` est l'utilisateur au nom duquel l'exécution s'est effectuée.

### Objet étape

| Champ | Type | Description |
|---|---|---|
| `node_id` | chaîne | Le nœud. |
| `node_type` | chaîne | Son type. |
| `status` | chaîne | `running`, `waiting`, `success`, `failed` ou `skipped`. |
| `started_at` | horodatage | Début, `null` pour une étape ignorée. |
| `finished_at` | horodatage | Fin. |
| `handle` | chaîne | Sortie sur laquelle l'exécution a continué : `null` (par défaut), une sortie nommée, ou `error`. |
| `output_json` | chaîne | Sortie de l'étape sous forme de texte JSON, plafonnée à 200 000 caractères (2 000 dans le corps d'une boucle). |
| `output_truncated` | booléen | La sortie a été coupée au plafond ; `output_json` n'est alors pas du JSON valide. |
| `text` | chaîne | Résultat lisible ou texte de statut. |
| `error` | chaîne | Raison de l'échec de l'étape. |
| `file_ids` | tableau d'UUID | Fichiers produits par l'étape. |
| `conversation_id` | UUID | Conversation créée par une étape `assistant.run` ; lisible via les [conversations](conversations.md). |
| `iteration` | tableau d'entiers | Pour une étape dans le corps d'une boucle, les index des éléments, de la boucle la plus externe vers l'intérieur ; vide sinon. Une étape dans une boucle apparaît une fois par élément. |

Exemple :

```json
{
  "id": "c2a1b0d9-8e7f-4a6b-9c5d-4e3f2a1b0c9d",
  "workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
  "organization_id": null,
  "version": null,
  "trigger_node_id": "start",
  "trigger_type": "trigger.manual",
  "trigger_depth": 0,
  "automatic": false,
  "input_json": "{\"customer\": \"Jana Novak\", \"question\": \"When will order 1042 be delivered?\"}",
  "status": "success",
  "started_at": 1759401220011,
  "active_since": 1759401220011,
  "finished_at": 1759401234567,
  "steps": [
    {
      "node_id": "start", "node_type": "trigger.manual", "status": "success",
      "started_at": 1759401220020, "finished_at": 1759401220031, "handle": null,
      "output_json": "{\"customer\": \"Jana Novak\", \"question\": \"When will order 1042 be delivered?\"}",
      "output_truncated": false, "text": "", "error": "", "file_ids": [], "conversation_id": null, "iteration": []
    },
    {
      "node_id": "answer", "node_type": "assistant.run", "status": "success",
      "started_at": 1759401220040, "finished_at": 1759401234400, "handle": null,
      "output_json": "{\"text\": \"Dear Jana, order 1042 ships on Friday ...\", \"data\": null, \"files\": []}",
      "output_truncated": false, "text": "Dear Jana, order 1042 ships on Friday ...", "error": "",
      "file_ids": [], "conversation_id": "8e2f4a6c-1b3d-4e5f-8a7b-9c0d1e2f3a4b", "iteration": []
    },
    {
      "node_id": "result", "node_type": "output", "status": "success",
      "started_at": 1759401234410, "finished_at": 1759401234420, "handle": null,
      "output_json": "{\"answer\": \"Dear Jana, order 1042 ships on Friday ...\"}",
      "output_truncated": false, "text": "", "error": "", "file_ids": [], "conversation_id": null, "iteration": []
    }
  ],
  "output_json": "{\"answer\": \"Dear Jana, order 1042 ships on Friday ...\"}",
  "error": "",
  "credits": 0.42,
  "created": {"timestamp": 1759401220000, "user_id": "0f1e2d3c-4b5a-4968-8776-655443322110"}
}
```

## Exécution en streaming

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/run/stream` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

Même requête que pour [exécuter un workflow](#executer-un-workflow), mais la réponse transmet en streaming les événements de
l'exécution au fur et à mesure. L'exécution se poursuit sur le serveur jusqu'à son terme même si le client
se déconnecte ; lisez ensuite le résultat avec [obtenir l'exécution](#executions).

Les erreurs d'accès et de validation (pas d'accès, brouillon invalide, entrée trop volumineuse) sont renvoyées sous forme
d'erreur HTTP normale avant le début du stream.

### Format de transmission

La réponse (`Content-Type: text/event-stream`) est une suite d'objets JSON écrits
les uns à la suite des autres sans séparateurs (pas de lignes SSE `data:`) ; voir
[streaming](streaming.md). Chaque objet est un chunk :

```json
{"timestamp": 1759401220012, "data": {"type": "run_start", "run_id": "c2a1b0d9-8e7f-4a6b-9c5d-4e3f2a1b0c9d", "node_id": null, "node_type": null, "status": "running", "handle": null, "text": "", "error": "", "output_json": "", "iteration": []}, "error": null}
```

- `data` est un événement de l'exécution (ci-dessous).
- `data: ""` est un chunk de maintien de connexion (keep-alive), envoyé environ chaque seconde tant qu'il ne se passe rien. Ignorez-le.
- Un chunk dont `error` est renseigné (`{"status", "text", "detail"}`) signale une erreur qui a mis fin au
  stream.

### Événements

Chaque événement a les champs `type`, `run_id`, `node_id`, `node_type`, `status`, `handle`,
`text`, `error`, `output_json` et `iteration` ; ceux qui portent une valeur dépendent du
type.

| `type` | Signification | Champs renseignés |
|---|---|---|
| `run_start` | L'exécution a démarré. | `status: "running"` |
| `step_start` | Une étape a démarré. | `node_id`, `node_type`, `status: "running"`, `iteration` |
| `step_progress` | Texte de progression d'une étape en cours (un outil ou un assistant au travail). | `node_id`, `node_type`, `text`, `iteration` |
| `step_waiting` | Une étape attend (une approbation). Les autres branches continuent. | `node_id`, `node_type`, `status: "waiting"`, `text`, `output_json` (pour une approbation `{approval_id, expires_at, assignees}`) |
| `step_end` | Une étape s'est terminée ou a été ignorée. | `node_id`, `node_type`, `status` (`success`, `failed`, `skipped`), `handle`, `text`, `error`, `output_json`, `iteration` |
| `run_waiting` | L'exécution est en pause ; **le stream se termine** sans `run_end`. | `status: "waiting"`, `text` (ids des étapes en attente, séparés par des virgules) |
| `run_end` | L'exécution est terminée ; dernier événement. | `status` (`success`, `failed`), `error`, `output_json` |

Les événements des branches parallèles et des éléments de boucle s'entremêlent ; utilisez `node_id` et `iteration` pour
les distinguer.

### Exemples

```bash
curl -N -X POST "https://ayeto.ai/api/v3/workflow/run/stream" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "input": {"question": "When will order 1042 be delivered?"}}'
```

Python, en découpant le stream en objets JSON avec `raw_decode` :

```python
import json
import os
import requests

decoder = json.JSONDecoder()

def events(response):
    """Yield the run events of a streamed response."""
    buffer = ""
    for piece in response.iter_content(chunk_size=None, decode_unicode=True):
        buffer += piece
        while True:
            buffer = buffer.lstrip()
            if not buffer:
                break
            try:
                chunk, end = decoder.raw_decode(buffer)
            except json.JSONDecodeError:
                break  # incomplete object, wait for more data
            buffer = buffer[end:]
            if chunk.get("error"):
                raise RuntimeError(chunk["error"]["detail"])
            if chunk.get("data"):
                yield chunk["data"]

with requests.post(
    "https://ayeto.ai/api/v3/workflow/run/stream",
    headers={"uni-api-key": os.environ["AYETO_API_KEY"]},
    json={"workflow_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a", "input": {"question": "When will order 1042 be delivered?"}},
    stream=True,
    timeout=(10, 120),
) as response:
    response.raise_for_status()
    response.encoding = "utf-8"
    for event in events(response):
        if event["type"] == "step_end":
            print(f"{event['node_id']}: {event['status']} {event['error']}")
        elif event["type"] == "run_waiting":
            print("paused, waiting for:", event["text"])
        elif event["type"] == "run_end":
            print("run", event["status"], event["output_json"] or event["error"])
```

JavaScript, en découpant le stream par le suivi des accolades hors des chaînes :

```javascript
async function* runEvents(response) {
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) return;
    buffer += value;
    let depth = 0, inString = false, escaped = false, start = -1, consumed = 0;
    for (let i = 0; i < buffer.length; i++) {
      const c = buffer[i];
      if (inString) {
        if (escaped) escaped = false;
        else if (c === "\\") escaped = true;
        else if (c === '"') inString = false;
      } else if (c === '"') inString = true;
      else if (c === "{") { if (depth++ === 0) start = i; }
      else if (c === "}" && --depth === 0) {
        const chunk = JSON.parse(buffer.slice(start, i + 1));
        consumed = i + 1;
        if (chunk.error) throw new Error(chunk.error.detail);
        if (chunk.data) yield chunk.data;
      }
    }
    buffer = buffer.slice(consumed);
  }
}

const response = await fetch("https://ayeto.ai/api/v3/workflow/run/stream", {
  method: "POST",
  headers: { "uni-api-key": process.env.AYETO_API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    workflow_id: "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    input: { question: "When will order 1042 be delivered?" },
  }),
});
if (!response.ok) throw new Error((await response.json()).detail);
for await (const event of runEvents(response)) {
  if (event.type === "step_start") console.log("running", event.node_id);
  if (event.type === "step_end") console.log(event.node_id, event.status, event.error);
  if (event.type === "run_waiting") console.log("paused, waiting for", event.text);
  if (event.type === "run_end") console.log("done", event.status, event.output_json || event.error);
}
```

## Annuler une exécution

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/run/cancel` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `run_id` | UUID | Oui | L'exécution. |

Annule une exécution qui est `waiting` ou `queued`. Ses étapes en attente échouent et leurs approbations
en attente sont annulées. L'utilisateur au nom duquel l'exécution s'est effectuée et les utilisateurs ayant un accès en écriture au
workflow peuvent l'annuler. Une exécution `running` ne peut pas être annulée (`422`) ; elle se termine
d'elle-même ou à la limite de durée d'exécution. Renvoie l'[exécution](#objet-execution) annulée.

## Exécutions

| | |
|---|---|
| Endpoints | `POST /api/v3/workflow/runs/find`<br>`POST /api/v3/workflow/runs/count`<br>`POST /api/v3/workflow/runs/get?entity_id=<run id>` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/runs/delete?entity_id=<run id>` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

Ces endpoints voient les exécutions **effectuées au nom de l'utilisateur de la clé API** : les exécutions que l'utilisateur a démarrées,
et les exécutions automatiques des workflows que l'utilisateur a publiés ou activés. Les exécutions des autres utilisateurs sont
signalées comme introuvables.

`find` renvoie un tableau d'[exécutions](#objet-execution) et `count` un entier ; les deux acceptent le
[corps de requête](conventions.md#interroger-des-listes). Les exécutions incluent toutes leurs étapes et sorties, paginez donc
toujours. `get` renvoie une exécution (corps vide). `delete` supprime une exécution avec les
conversations et fichiers créés par ses étapes ainsi que ses approbations, et renvoie son id ; supprimer une
exécution en pause y met fin.

Filtres utiles :

| Objectif | Filtre |
|---|---|
| Exécutions d'un workflow | `["workflow_id", "==", "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a"]` |
| Exécutions en pause | `["status", "==", "waiting"]` |
| Exécutions en échec | `["status", "==", "failed"]` |
| Exécutions issues du webhook | `["trigger_type", "==", "trigger.webhook"]` |
| Exécutions de la version publiée uniquement | `["version", "!=", null]` |
| Démarrées depuis un instant donné | `["created.timestamp", ">=", 1759363200000]` |

```bash
curl -X POST "https://ayeto.ai/api/v3/workflow/runs/find" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters": [["workflow_id", "==", "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a"]], "sort_key": "created.timestamp", "sort_order": 1, "limit_from": 0, "limit_to": 10}'
```

Pour suivre une exécution en pause ou en file d'attente, interrogez `get` jusqu'à ce que son `status` soit `success`, `failed` ou
`cancelled`.

## Approbations

Les endpoints d'approbation servent aux personnes qui décident : tout utilisateur désigné par une étape d'approbation peut
les utiliser, sans autre permission sur le workflow. La clé API doit tout de même avoir la portée
`ayeto.workflow`.

| | |
|---|---|
| Endpoints | `POST /api/v3/workflow/approval/find`<br>`POST /api/v3/workflow/approval/get`<br>`POST /api/v3/workflow/approval/decide` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/approval/count` |
| Portée | `ayeto.workflow` |
| Limite de débit | [élevée](conventions.md#limites-de-debit) |

### Objet approbation

Champs communs, plus :

| Champ | Type | Description |
|---|---|---|
| `workflow_id` | UUID | Le workflow. |
| `workflow_name` | chaîne | Son nom au moment de l'ouverture de l'approbation. |
| `run_id` | UUID | L'exécution en pause. |
| `node_id` | chaîne | L'étape d'approbation. |
| `organization_id` | UUID | Organisation du workflow, ou `null`. |
| `status` | chaîne | `pending`, `approved`, `rejected`, `expired` (personne n'a décidé à temps) ou `cancelled` (l'exécution s'est terminée avant). |
| `title` | chaîne | Ce qu'il faut décider. |
| `message` | chaîne | Détails (Markdown). |
| `form_schema` | objet | Schéma JSON du formulaire (`{"type": "object", "properties": {...}, "required": [...]}`) avec des champs plats de type `string`, `number`, `integer`, `boolean`, ou un fichier (`{"type": "object", "format": "file"}`) ; `null` lorsqu'il n'y a pas de formulaire. |
| `form_values` | objet | Valeurs préremplies, par nom de champ. |
| `allow_reject` | booléen | L'approbation peut être rejetée. Lorsque `false`, l'étape ne fait que recueillir le formulaire. |
| `assignee_user_ids` | tableau d'UUID | Utilisateurs qui peuvent décider. |
| `expires_at` | horodatage | Échéance. |
| `timeout_continues` | booléen | À l'expiration, l'exécution continue sur `timeout` ; sinon l'étape échoue. |
| `decision_data` | objet | Les données du formulaire de la décision. |
| `comment` | chaîne | Commentaire de la décision. |
| `decided_by` | objet | `{id, email, name}` de la personne qui a décidé, ou `null`. |
| `decided_at` | horodatage | Moment de la décision ou de l'expiration. |

### Lister les approbations

Requête :

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `status` | chaîne | Non | Uniquement les approbations ayant ce statut, par exemple `pending`. |
| `limit_from` | entier | Non | Index du premier élément. Par défaut `0`. |
| `limit_to` | entier | Non | Index suivant le dernier élément, de 1 à 200. Par défaut `50`. |

Renvoie `{"items": [<approval>, ...], "total": <number>}` avec les approbations dont l'utilisateur peut
décider (ou a décidé), de la plus récente à la plus ancienne. `total` est le nombre de toutes les approbations correspondantes.

### Compter les approbations en attente

Pas de corps de requête. Renvoie `{"pending": 3}`, le nombre d'approbations qui attendent la
décision de l'utilisateur. Adapté à l'interrogation périodique d'un badge.

### Obtenir une approbation

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `approval_id` | UUID | Oui | L'approbation. |

Renvoie l'[approbation](#objet-approbation). Les utilisateurs qui peuvent en décider et ceux qui peuvent lire
son workflow peuvent l'obtenir.

### Décider d'une approbation

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `approval_id` | UUID | Oui | L'approbation. |
| `decision` | chaîne | Oui | `approve` ou `reject`. |
| `data` | objet | Non | Données du formulaire (approbation uniquement). Les champs que vous n'envoyez pas conservent leurs valeurs préremplies ; les champs absents du formulaire sont ignorés. Au plus 20 000 caractères en JSON. Les champs fichier acceptent la forme base64 décrite dans [fichiers dans l'entrée d'une exécution](#fichiers-dans-lentree-dune-execution). |
| `comment` | chaîne | Non | Commentaire, au plus 2 000 caractères. |

Seuls les utilisateurs de `assignee_user_ids` peuvent décider, et uniquement tant que l'approbation est `pending` et
avant `expires_at`. Les données du formulaire sont validées par rapport à `form_schema`. La décision devient
la sortie de l'étape d'approbation :

```json
{
  "decision": "approved",
  "data": {"amount": 1200, "note": "OK for this quarter"},
  "comment": "Approved",
  "decided_by": {"id": "0f1e2d3c-4b5a-4968-8776-655443322110", "email": "jana@example.com", "name": "Jana Novak"},
  "decided_at": 1759405000000,
  "approval_id": "e4d3c2b1-a0f9-4e8d-9c7b-6a5f4e3d2c1b"
}
```

L'exécution reprend en arrière-plan au nom de l'utilisateur au nom duquel elle s'effectuait (et non de celui qui a décidé) ; son
statut passe de `waiting` à `queued` puis à `running`. Renvoie l'[approbation](#objet-approbation)
décidée.

```bash
curl -X POST "https://ayeto.ai/api/v3/workflow/approval/decide" \
  -H "uni-api-key: $AYETO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "approval_id": "e4d3c2b1-a0f9-4e8d-9c7b-6a5f4e3d2c1b",
    "decision": "approve",
    "data": {"amount": 1200, "note": "OK for this quarter"},
    "comment": "Approved"
  }'
```

## Publication et déclencheurs

Ces endpoints nécessitent un accès en écriture au workflow.

### Informations de déploiement

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/deployment` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |

Renvoie l'état de publication. Il contient les URL des webhooks avec leurs secrets, c'est pourquoi
il nécessite la portée d'écriture.

| Champ | Type | Description |
|---|---|---|
| `published_version` | entier | Dernière version publiée, ou `null`. |
| `published_at` | horodatage | Date de publication. |
| `active` | booléen | Les déclencheurs automatiques sont activés. |
| `run_as` | UUID | Utilisateur au nom duquel s'effectuent les exécutions automatiques. |
| `draft_changed` | booléen | Le brouillon diffère de la version publiée (positions des nœuds mises à part) ; toujours `true` lorsque rien n'est publié. |
| `triggers` | tableau d'objets | Déclencheurs automatiques de la version publiée, voir ci-dessous. |
| `signing_secret` | chaîne | Clé qui signe les requêtes des étapes `http.request` avec `sign: true` ; vide lorsqu'aucune étape ne signe. |

Champs d'un déclencheur :

| Champ | Type | Description |
|---|---|---|
| `node_id` | chaîne | Le nœud déclencheur. |
| `type` | chaîne | `trigger.schedule`, `trigger.webhook` ou `trigger.booster_record`. |
| `active` | booléen | Le déclencheur se déclenche. |
| `next_run_at` | horodatage | Prochaine heure planifiée (déclencheurs de planning). |
| `last_fired_at` | horodatage | Dernière fois qu'il a démarré une exécution. |
| `url` | chaîne | URL du webhook, secret inclus (déclencheurs webhook). |
| `panel_id` | UUID | Panneau Booster (déclencheurs d'enregistrements booster). |
| `collection` | chaîne | Collection surveillée, `*` pour toutes (déclencheurs d'enregistrements booster). |

Une étape `http.request` signée envoie l'en-tête `X-Ayeto-Signature: sha256=<hex HMAC-SHA256 of the
request body>` calculé avec `signing_secret`, afin que le service destinataire puisse le vérifier.

### Publier un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/publish` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |
| `note` | chaîne | Non | Note de la version, au plus 500 caractères. |

Valide le brouillon pour l'appelant, l'enregistre comme version suivante, active le workflow
et fait de l'appelant l'utilisateur `run_as` des exécutions automatiques. Les URL de webhook des nœuds déclencheurs
qui existaient auparavant restent les mêmes. Renvoie les [informations de déploiement](#informations-de-deploiement). Un
brouillon invalide est rejeté avec `422`.

### Activer ou désactiver

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/activate` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |
| `active` | booléen | Oui | `true` active les déclencheurs automatiques, `false` les désactive. |

L'activation valide la version publiée pour l'appelant et fait de l'appelant l'utilisateur
`run_as`. Tant que le workflow est inactif, les appels de webhook renvoient `404` et les plannings ne se déclenchent pas.
Renvoie les [informations de déploiement](#informations-de-deploiement). Échoue avec `422` lorsque rien n'est publié.

### Régénérer le secret d'un webhook

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/trigger/regenerate` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [faible](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |
| `node_id` | chaîne | Oui | Le nœud déclencheur webhook de la version publiée. |

Attribue un nouveau secret au webhook ; l'ancienne URL cesse immédiatement de fonctionner. Renvoie les
[informations de déploiement](#informations-de-deploiement) avec la nouvelle URL.

### Prévisualiser un planning

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/schedule/preview` |
| Portée | `ayeto.workflow` |
| Limite de débit | [élevée](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `cron` | chaîne | Oui | Expression cron à cinq champs : minute, heure, jour du mois, mois, jour de la semaine (par exemple `0 8 * * 1-5`). Au plus 200 caractères. |
| `timezone` | chaîne | Non | Fuseau horaire IANA, par exemple `Europe/Prague`. Par défaut `UTC`. |

Vérifie un planning pour un nœud `trigger.schedule` et liste ses prochaines occurrences :

```json
{"valid": true, "error": "", "next_runs": [1759471200000, 1759730400000, 1759816800000, 1759903200000, 1759989600000]}
```

Un planning invalide renvoie `200` avec `valid: false` et la raison dans `error` (nombre de champs
incorrect, fuseau horaire inconnu, exécutions plus fréquentes que l'intervalle minimal autorisé).

## Assistant builder

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/assistant` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |

Chaque utilisateur ayant un accès en écriture dispose de son propre assistant builder pour un workflow : un
[assistant](assistants.md) qui lit, modifie, exécute en test et explique le workflow au fil de la
conversation. Cet endpoint renvoie le builder de l'appelant (en le créant s'il n'existe pas) sous forme
d'[assistant](assistants.md). Dialoguez avec lui via le [chat](chat.md) en utilisant son `id` comme
assistant ; cela nécessite une clé avec la portée chat. Ses modifications sont enregistrées comme révisions d'origine
`builder`.

## Révisions

Chaque modification de la définition est une révision. Les modifications qui ne font que déplacer des nœuds, effectuées par le même
utilisateur en moins de trois minutes, sont fusionnées dans la dernière révision. Les révisions les plus récentes sont conservées
(100 par défaut).

### Lister les révisions

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/revisions` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |

Renvoie jusqu'à 100 révisions, de la plus récente à la plus ancienne, sans leurs définitions :

| Champ | Type | Description |
|---|---|---|
| `id` | UUID | La révision. |
| `created_at` | horodatage | Date de création. |
| `user_id` | UUID | Auteur. |
| `origin` | chaîne | `created`, `editor` (l'éditeur de l'application ou cette API), `builder` (l'assistant builder) ou `restore`. |
| `changes` | objet | `{added_nodes, removed_nodes, changed_nodes, added_edges, removed_edges}` : listes d'id de nœuds et nombres d'arêtes par rapport à la révision précédente. |
| `restored_from` | UUID | Pour une restauration, la révision qui a été rétablie. |
| `nodes` | entier | Nombre de nœuds. |
| `edges` | entier | Nombre d'arêtes. |

### Obtenir une révision

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/revision/get` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |
| `revision_id` | UUID | Oui | La révision. |

Renvoie la révision avec sa définition complète : les champs communs plus `workflow_id`,
`definition`, `origin`, `changes` et `restored_from`.

### Restaurer une révision

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/revision/restore` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

Même requête que pour [obtenir une révision](#obtenir-une-revision). Rétablit la définition de la révision comme
brouillon ; la restauration est enregistrée comme nouvelle révision (origine `restore`), elle peut donc être annulée.
La version publiée ne change pas. Renvoie le [workflow](#objet-workflow) mis à jour.

## Export et import

Un export est un document JSON contenant la définition du brouillon, le nom et la description. Les exécutions,
les révisions, les versions publiées, l'état des déclencheurs (secrets des webhooks), le secret de signature,
l'organisation et les partages ne sont pas exportés. Les assistants auxquels les étapes font référence sont exportés uniquement
sous forme d'id.

### Exporter un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/export` |
| Portée | `ayeto.workflow` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow. |
| `strip_secrets` | booléen | Non | Ignoré sur cette API : les identifiants d'accès sont toujours supprimés. |

Via une clé API, les valeurs des en-têtes des étapes `http.request` dont le nom ressemble à un
identifiant d'accès (contient `auth`, `token`, `secret`, `key`, `password`, `cookie`, `signature` ou
`session`) sont toujours vidées ; les noms des en-têtes sont conservés. Les chemins vidés sont listés dans
`secret_keys`. L'utilisateur a besoin d'un accès en écriture, ou d'un partage en lecture avec la permission
`workflow.export`.

```json
{
  "version": "1.0",
  "exported_at": 1759405000000,
  "workflow": {
    "name": "Customer question",
    "description": "Drafts an answer to a customer question",
    "definition": {"nodes": ["..."], "edges": ["..."]},
    "from_id": "5d4c3b2a-1f0e-4d9c-8b7a-6f5e4d3c2b1a",
    "secret_keys": ["notify_crm.headers.Authorization"]
  }
}
```

### Importer un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/import` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `data` | objet | Oui | Un document d'export, tel que renvoyé par l'[export](#exporter-un-workflow). |
| `new_name` | chaîne | Non | Nom du nouveau workflow, au plus 200 caractères. Par défaut : le nom exporté. |
| `organization_id` | UUID | Non | Le créer dans cette organisation (accès en écriture requis). Par défaut : personnel. |

Crée un nouveau workflow non publié appartenant à l'appelant et le valide. Les étapes qui font référence à
des éléments que l'appelant ne peut pas utiliser (un assistant d'un autre compte, des identifiants d'accès vidés) ne
bloquent pas l'import ; elles sont signalées afin de pouvoir être corrigées.

| Champ | Type | Description |
|---|---|---|
| `workflow` | objet | Le nouveau [workflow](#objet-workflow). |
| `issues` | tableau d'objets | Problèmes de validation pour l'appelant, comme dans [valider](#valider-un-workflow). |
| `secret_keys` | tableau de chaînes | Valeurs d'en-têtes à renseigner (`<node id>.headers.<name>`). |

Le format d'export doit avoir la même version majeure (`1`), et la définition au plus 200
nœuds.

### Copier un workflow

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/copy` |
| Portée | `ayeto.workflow.write` |
| Limite de débit | [par défaut](conventions.md#limites-de-debit) |

| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| `workflow_id` | UUID | Oui | Le workflow à copier. |
| `new_name` | chaîne | Non | Nom de la copie. Par défaut : le nom d'origine. |
| `organization_id` | UUID | Non | Organisation de la copie. Par défaut : personnel. |

Exporte le brouillon et l'importe comme nouveau workflow de l'appelant en un seul appel ; la réponse
est la même que pour l'[import](#importer-un-workflow). L'utilisateur a besoin d'un accès en écriture ou d'un partage en lecture
avec la permission `workflow.copy`. Un utilisateur ayant un accès en écriture à l'original conserve les
identifiants d'accès des étapes HTTP ; une copie effectuée grâce à la permission `workflow.copy` les
omet.

## Webhooks

| | |
|---|---|
| Endpoint | `POST /api/v3/workflow/hook/{trigger_id}/{secret}` |
| Portée | aucune : le secret contenu dans l'URL autorise l'appel |
| Limite de débit | [webhook](conventions.md#limites-de-debit) par IP, plus une limite par déclencheur (ci-dessous) |

Un nœud `trigger.webhook` d'un workflow publié et actif possède une URL publique. Obtenez-la depuis
`triggers[].url` des [informations de déploiement](#informations-de-deploiement) :

```
https://ayeto.ai/api/v3/workflow/hook/9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/Xq3v...secret...
```

Traitez l'URL comme un secret ; toute personne qui la détient peut démarrer des exécutions qui dépensent les crédits de
l'utilisateur `run_as`. En cas de fuite, [régénérez](#regenerer-le-secret-dun-webhook)-la.

### Requête

Envoyez un `POST` avec un corps JSON et `Content-Type: application/json`. Un corps d'un autre
type de contenu est transmis sous forme de texte (ou analysé, s'il s'agit de JSON). Aucune clé API n'est nécessaire.

L'entrée de l'exécution (la sortie du déclencheur, disponible pour les étapes sous la forme `{{ input }}`) est :

| Champ | Type | Description |
|---|---|---|
| `body` | tout type | Le corps de la requête ; `{}` lorsqu'il est vide. |
| `query` | objet | Paramètres de la chaîne de requête de l'URL. |
| `headers` | objet | Uniquement ces en-têtes de requête, lorsqu'ils sont présents : `content-type`, `user-agent`, `x-github-event`, `x-request-id`. |

Les étapes la lisent sous la forme `{{ input.body.order_id }}`, `{{ input.query.source }}` et ainsi de suite. L'entrée
est limitée à 200 000 caractères de JSON.

### Fichiers dans le corps

Des fichiers peuvent être envoyés dans le corps, à n'importe quelle profondeur (jusqu'à 32 niveaux) :

- un objet avec des `data` en base64 et un `name` ou `filename`, et aucune autre clé que `name`,
  `filename`, `content_type`, `mime_type`, `data`, `size` ;
- une chaîne data URL `data:<type>;base64,...`.

Chaque fichier est enregistré comme fichier de l'exécution (appartenant à l'utilisateur `run_as`, soumis à la limite de taille
des uploads et au quota de stockage) et remplacé dans le corps par ses métadonnées de pièce jointe
(`{file_id, filename, content_type, file_url, size}`). Au plus 10 fichiers par appel.

### Réponse

`200 OK` dès que l'exécution est mise en file d'attente ; l'exécution s'effectue en arrière-plan :

```json
{"run_id": "b7c6d5e4-f3a2-4b1c-9d8e-7f6a5b4c3d2e", "status": "queued"}
```

L'exécution exécute la version publiée au nom de l'utilisateur `run_as`, qui la voit via les
[exécutions](#executions) (`trigger_type` vaut `trigger.webhook`, `automatic` vaut `true`). Son entrée n'est pas
considérée comme fiable : une étape ne lit les fichiers de l'utilisateur que lorsque l'étape les désigne elle-même, jamais parce que
le corps du webhook les désigne.

### Limitation de débit

Chaque déclencheur accepte au plus 60 appels par minute, 2 000 par heure et 20 000 par jour, quel que soit
le nombre d'expéditeurs. Une limite anti-saturation par IP s'applique en plus. Un workflow refuse aussi les nouvelles
exécutions tant qu'il a 50 exécutions en file d'attente (par défaut). Les deux répondent `429` ; une réponse de limite de débit
comporte `Retry-After` (en secondes).

### Exemple

```bash
curl -X POST "https://ayeto.ai/api/v3/workflow/hook/9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d/$WEBHOOK_SECRET?source=shop" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": 1042,
    "customer": {"name": "Jana Novak", "email": "jana@example.com"},
    "invoice": {"filename": "invoice-1042.pdf", "content_type": "application/pdf", "data": "JVBERi0xLjcKJcfsj6IK..."}
  }'
```

## Erreurs

Les erreurs ont le corps JSON `{"detail": "..."}`. Les erreurs d'authentification, de limite de débit et les erreurs serveur
sont décrites dans les [conventions](conventions.md#erreurs).

| Statut | Signification |
|---|---|
| `401` | La clé API est absente, invalide ou n'a pas la portée requise (`API key is invalid`). |
| `403` | L'utilisateur n'a pas accès au workflow, ou n'a pas la permission ou l'appartenance à l'organisation requise par l'opération. |
| `404` | Le workflow, l'exécution, la révision, l'approbation ou le webhook n'existe pas ou n'est pas visible pour l'utilisateur. |
| `422` | La requête est invalide, ou l'opération n'est pas possible dans l'état actuel (`detail` en indique la raison). |
| `423` | Le workflow ou l'exécution est en cours de modification par une autre requête ; réessayez dans un instant. |
| `429` | Trop de requêtes (avec `Retry-After`), ou trop d'exécutions en file d'attente. |
| `500` | Erreur serveur. |

Erreurs propres aux workflows :

| Statut | `detail` | Cause |
|---|---|---|
| `403` | `permission denied` | Le compte de l'utilisateur n'est pas autorisé à lire ou à écrire des workflows ou des exécutions. |
| `403` | `You do not have access to this workflow` | L'utilisateur n'est pas propriétaire et le workflow n'est pas partagé avec lui (ou il est partagé en lecture alors que l'écriture est nécessaire). |
| `403` | `This workflow is shared with you without the 'workflow.run' permission` | Partage en lecture sans la permission d'exécuter (de même pour `workflow.export`, `workflow.copy`). |
| `403` | `User is not a member of the organization` | Le workflow appartient à une organisation dont l'utilisateur ne fait pas partie. |
| `403` | `User does not have write permissions in the organization` | L'utilisateur ne peut pas travailler dans l'organisation du workflow. |
| `404` | `Workflow not found` | Le workflow n'existe pas (ou a été supprimé). |
| `404` | `entity not found` | `get`, `update`, `delete` CRUD d'un workflow ou d'une exécution qui n'existe pas ou n'appartient pas à l'utilisateur. |
| `404` | `Workflow run not found` | Exécution inconnue, ou exécution que l'utilisateur ne peut pas annuler. |
| `404` | `Revision not found` | La révision n'existe pas ou appartient à un autre workflow. |
| `404` | `Approval not found` | Approbation inconnue, ou l'utilisateur ne peut ni la voir ni en décider. |
| `404` | `The file of the input '<key>' was not found or is not accessible` | Un champ fichier désigne un fichier que l'utilisateur ne possède pas. |
| `404` | `The published workflow has no such webhook trigger` | Régénération avec un nœud qui n'est pas un déclencheur webhook de la version publiée. |
| `404` | `Webhook not found` | Id de déclencheur inconnu ou secret erroné. |
| `404` | `The workflow of this webhook is not active` | Le workflow est désactivé ou non publié, ou il a été supprimé. |
| `422` | `The workflow is not valid: <errors>` | Exécution, publication ou activation d'une définition comportant des erreurs de validation. |
| `422` | `'<id>' is not a trigger of the workflow` | `trigger_node_id` n'est pas un déclencheur. |
| `422` | `The workflow has no trigger` | Aucun déclencheur à partir duquel démarrer. |
| `422` | `The run input is too large (at most 200000 characters of JSON)` | Entrée d'exécution ou entrée de webhook au-delà de la limite. |
| `422` | `The input '<key>' must be one file` | Un champ fichier contient plusieurs fichiers ou autre chose. |
| `422` | `The file is not valid base64 data` | Fichier transmis directement avec un base64 invalide. |
| `422` | `Too many files (at most 10)` | Trop de fichiers dans le corps d'un webhook. |
| `422` | `A <status> run can not be cancelled` | Annulation d'une exécution qui n'est ni `waiting` ni `queued`. |
| `422` | `Publish the workflow first` | Activation sans version publiée. |
| `422` | `Unsupported workflow export format <version>` | Import d'un export ayant une autre version majeure. |
| `422` | `The workflow has too many steps (<n>, at most 200)` | Import d'une définition dépassant la limite de nœuds. |
| `422` | `The approval is <status> already` | Décision sur une approbation qui n'est plus en attente. |
| `422` | `The time to decide the approval is over` | Décision après `expires_at`. |
| `422` | `The approval can not be rejected` | `reject` alors que `allow_reject` vaut `false`. |
| `422` | `Step '<node id>' of the run does not wait` | L'exécution a cessé d'attendre (annulée, en échec) avant la décision. |
| `422` | `The form data is too large (at most 20000 characters of JSON)` | Données de décision au-delà de la limite. |
| `429` | `Too many requests` | Limite de débit, y compris la limite de webhook par déclencheur ; voir `Retry-After`. |
| `429` | `The workflow already has <n> runs waiting` | Appel de webhook alors que la file d'attente du workflow est pleine. |

L'échec d'une étape n'est pas une erreur HTTP : l'exécution revient avec `status: "failed"` et la
raison dans `error` ainsi que dans le champ `error` de l'étape en échec.
