Status : IMPLEMENTEE
Un systeme ou l'utilisateur (ou l'IA) peut construire des routines = enchainements d'actions avec du control flow (if/else, for, while), ou chaque node recoit les outputs des nodes precedents.
┌──────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────┐
│ Trigger │───►│ Gmail: list │───►│ For each │───►│ Output │
│ (cron/ │ │ messages │ │ message │ │ resultat │
│ manual/ │ │ │ │ ┌────────┐ │ │ │
│ webhook)│ │ out: items[] │ │ │Get msg │ │ │ │
└──────────┘ └──────────────┘ │ │details │ │ └──────────┘
│ └───┬────┘ │
│ │ │
│ ┌───▼────┐ │
│ │If from │ │
│ │contains│ │
│ │"amazon"│ │
│ └───┬────┘ │
│ Y │ N │
│ ┌───▼────┐ │
│ │Notify │ │
│ │push │ │
│ └────────┘ │
└─────────────┘
3 modes de creation :
routinesCREATE TABLE connectors.routines (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL,
name varchar(200) NOT NULL,
description text,
-- Le graph complet des nodes (voir schema ci-dessous)
nodes jsonb NOT NULL DEFAULT '[]',
-- Connections entre nodes
edges jsonb NOT NULL DEFAULT '[]',
-- Variables globales de la routine (scope partage)
variables jsonb DEFAULT '{}',
-- Trigger config
trigger_type varchar(20) NOT NULL DEFAULT 'manual', -- manual, cron, webhook, event
trigger_config jsonb DEFAULT '{}',
-- Etat
is_active boolean DEFAULT false,
last_run_at timestamp,
last_status varchar(20), -- success, error, running
-- Meta
tags text[],
created_at timestamp DEFAULT now(),
updated_at timestamp DEFAULT now()
);
routine_executionsCREATE TABLE connectors.routine_executions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
routine_id uuid NOT NULL REFERENCES connectors.routines(id),
user_id uuid NOT NULL,
status varchar(20) NOT NULL DEFAULT 'running', -- running, success, error, cancelled
trigger_type varchar(20),
-- Log d'execution de chaque node (ordered)
node_results jsonb DEFAULT '[]',
-- Variables finales apres execution
variables jsonb DEFAULT '{}',
error_message text,
started_at timestamp DEFAULT now(),
completed_at timestamp,
duration_ms integer
);
Chaque node est un objet JSON dans le tableau nodes :
interface RoutineNode {
id: string; // ex: "node_1", "node_2"
type: NodeType;
label: string; // Nom affiche dans l'editeur
config: NodeConfig; // Configuration specifique au type
position?: { x: number; y: number }; // Position dans l'editeur visuel
}
type NodeType =
| 'trigger' // Point d'entree (cron, webhook, manual, event)
| 'action' // Execute une action existante
| 'fetch' // Appel API direct (sans action pre-definie)
| 'transform' // Extrait/transforme des donnees
| 'condition' // If/else (branchement)
| 'loop' // For each / while
| 'delay' // Attendre N secondes
| 'output' // Resultat final / notification
| 'variable' // Set/get variable
interface RoutineEdge {
id: string;
from: string; // node_id source
to: string; // node_id destination
label?: string; // Pour les conditions: "true" / "false"
port?: string; // Pour les loops: "body" / "done"
}
action — Execute une action existante{
action_id: string; // Reference vers actions table
input_mapping: Record<string, string>;// Map: param_name → expression
}
// Exemple:
{
action_id: "uuid-gmail-list",
input_mapping: {
maxResults: "3",
q: "{{trigger.params.search_query}}" // Vient du trigger
}
}
fetch — Appel API direct{
connector: string;
instance: string;
method: string;
path: string; // Supporte {{expressions}}
body?: any;
}
// Exemple:
{
connector: "gmail",
instance: "Gmail",
method: "GET",
path: "/gmail/v1/users/me/messages/{{nodes.loop.item.id}}?format=metadata"
}
condition — If/else{
// Expression evaluee comme truthy/falsy
expression: string;
// Operateur + valeur pour comparaison simple
operator?: 'equals' | 'contains' | 'gt' | 'lt' | 'exists' | 'matches';
left: string; // Expression: "{{nodes.get_msg.output.from}}"
right?: string; // Valeur: "amazon" ou "{{variables.threshold}}"
}
2 sorties : edge avec label: "true" et edge avec label: "false"
loop — For each / While{
loop_type: 'for_each' | 'while';
// For each: itere sur un tableau
items_expression?: string; // "{{nodes.list_msgs.output.messages}}"
// While: condition de continuation
while_expression?: string; // "{{variables.counter}} < 10"
// Securite
max_iterations: number; // Default: 50, hard limit: 200
}
Le corps du loop = les nodes connectes via port: "body".
L'item courant est accessible via {{nodes.LOOP_ID.item}} et l'index via {{nodes.LOOP_ID.index}}.
transform — Extraire/transformer des donnees{
// Expression d'entree
input: string; // "{{nodes.get_msg.output}}"
// Operations chainables
operations: TransformOp[];
}
type TransformOp =
| { type: 'pick'; fields: string[] } // Garder certains champs
| { type: 'get'; path: string } // Extraire un sous-champ (dot notation)
| { type: 'map'; expression: string } // Transformer chaque element
| { type: 'filter'; expression: string } // Filtrer un tableau
| { type: 'join'; separator: string } // Joindre un tableau en string
| { type: 'template'; template: string } // Template string
| { type: 'regex'; pattern: string; group?: number }
output — Resultat final / notification{
output_type: 'result' | 'notify' | 'variable';
// result: stocke dans routine_executions.node_results
// notify: envoie une notification (push/mail via notify_send)
// variable: set une variable globale de la routine
value: string; // Expression template
notify_channel?: string; // Pour notify: 'push', 'email'
variable_name?: string; // Pour variable: nom de la variable
}
trigger — Point d'entree{
trigger_type: 'manual' | 'cron' | 'webhook' | 'event';
cron_expression?: string; // "0 8 * * *" (tous les jours a 8h)
webhook_path?: string; // "/hooks/routine-xyz"
event_source?: string; // Futur: "gmail.new_message", "github.push"
params_schema?: object; // JSON Schema des params d'entree (manual/webhook)
}
Syntaxe template {{...}} pour referencer les donnees :
| Expression | Description |
|---|---|
{{trigger.params.x}} |
Parametre du trigger |
{{nodes.NODE_ID.output}} |
Output complet d'un node |
{{nodes.NODE_ID.output.field}} |
Champ specifique |
{{nodes.NODE_ID.output.items[0].name}} |
Acces tableau + champ |
{{nodes.LOOP_ID.item}} |
Item courant dans un loop |
{{nodes.LOOP_ID.index}} |
Index courant (0-based) |
{{variables.NAME}} |
Variable globale de la routine |
{{env.DATE}} |
Date courante ISO |
{{env.TIMESTAMP}} |
Timestamp unix |
Evaluation : simple template engine maison, pas d'eval(). Parse les {{}}, resolve les paths avec dot-notation, retourne la valeur.
routineExecutor)executeRoutine(routineId, userId, triggerParams?) → RoutineExecution
routine_execution (status: running)trigger (racine du graph)context = { nodes: {}, variables: {}, trigger: {} }context.nodes[node.id].output
d. Suivre les edges sortantes vers les nodes suivantscondition : suivre uniquement l'edge "true" ou "false"loop : executer le sous-graph du body pour chaque itemroutine_execution (status: success/error)| Limite | Valeur | Raison |
|---|---|---|
| Max nodes par routine | 50 | Eviter les routines trop complexes |
| Max iterations par loop | 200 | Eviter les boucles infinies |
| Max execution time | 5 min | Timeout global |
| Max API calls par execution | 30 | Eviter le spam API |
| Max routines actives (cron) | 20 par user | Limiter la charge |
type=routine[TOOL_CALL: type=routine | action=create | params={"name":"...", "nodes":[...], "edges":[...]}]
[TOOL_CALL: type=routine | action=execute | id=ROUTINE_ID | params={"search":"amazon"}]
[TOOL_CALL: type=routine | action=list]
Ajouter dans buildUserTools() :
Format Routine: [TOOL_CALL: type=routine | action=create | params={...}]
Tu peux creer des routines pour automatiser des taches. Une routine est un enchainement
d'actions avec du control flow. Exemples :
- "previens-moi quand je recois un mail d'Amazon" → routine cron qui check gmail + filtre + notify
- "sauvegarde mes repos github tous les jours" → routine cron qui list repos + for each + ssh backup
L'utilisateur dit : "previens-moi quand je recois un mail de la banque"
L'IA genere :
{
"name": "Alerte mail banque",
"trigger_type": "cron",
"trigger_config": { "cron_expression": "*/15 * * * *" },
"nodes": [
{ "id": "trigger", "type": "trigger", "label": "Toutes les 15 min", "config": { "trigger_type": "cron" }},
{ "id": "list", "type": "fetch", "label": "Lister mails recents", "config": {
"connector": "gmail", "instance": "Gmail", "method": "GET",
"path": "/gmail/v1/users/me/messages?maxResults=5&q=newer_than:15m"
}},
{ "id": "loop", "type": "loop", "label": "Pour chaque mail", "config": {
"loop_type": "for_each", "items_expression": "{{nodes.list.output.messages}}", "max_iterations": 10
}},
{ "id": "get_detail", "type": "fetch", "label": "Details du mail", "config": {
"connector": "gmail", "instance": "Gmail", "method": "GET",
"path": "/gmail/v1/users/me/messages/{{nodes.loop.item.id}}?format=metadata&metadataHeaders=Subject&metadataHeaders=From"
}},
{ "id": "check_bank", "type": "condition", "label": "Mail de la banque ?", "config": {
"operator": "contains",
"left": "{{nodes.get_detail.output.payload.headers[0].value}}",
"right": "banque"
}},
{ "id": "notify", "type": "output", "label": "Notification", "config": {
"output_type": "notify", "notify_channel": "push",
"value": "Mail de la banque : {{nodes.get_detail.output.snippet}}"
}}
],
"edges": [
{ "id": "e1", "from": "trigger", "to": "list" },
{ "id": "e2", "from": "list", "to": "loop" },
{ "id": "e3", "from": "loop", "to": "get_detail", "port": "body" },
{ "id": "e4", "from": "get_detail", "to": "check_bank" },
{ "id": "e5", "from": "check_bank", "to": "notify", "label": "true" }
]
}
Une routine produit des logs a 3 niveaux :
routine_executions)Comme action_executions mais pour la routine entiere :
routine_executions
├── id, routine_id, user_id
├── status: running | success | error | cancelled
├── trigger_type: manual | cron | webhook | ai_chat
├── trigger_params: {...} -- Params d'entree
├── node_results: [...] -- Resultat de chaque node (voir niveau 2)
├── variables: {...} -- Variables finales
├── error_message, error_node_id -- Quel node a plante
├── started_at, completed_at, duration_ms
└── total_api_calls: number -- Compteur API calls consommes
node_results dans routine_executions)Chaque node loggue son execution dans le tableau node_results :
interface NodeExecutionResult {
node_id: string;
node_type: string;
node_label: string;
status: 'success' | 'error' | 'skipped'; // skipped = condition false
input_resolved: any; // Inputs apres resolution des expressions
output: any; // Resultat du node
error?: string;
started_at: string;
duration_ms: number;
// Pour les loops : resultats de chaque iteration
iterations?: NodeExecutionResult[][];
}
action_executions)Quand un node action ou fetch s'execute, il cree un enregistrement dans action_executions avec :
trigger_type: 'routine'routine_execution_id: <uuid> (lien vers le niveau 1)Ca veut dire que les logs des routines sont compatibles avec les logs des actions existantes. Un meme ecran d'historique peut afficher les deux.
/routines/:id/history → Liste des executions
/routines/:id/history/:exec_id → Timeline detaillee node par node
┌─ Execution #42 — 31/01/2026 17:45 — ✓ Success (1.2s) ──────────────┐
│ │
│ [trigger] ✓ 0ms │
│ [list_mails] ✓ 120ms → 3 messages │
│ [loop] 3 iterations │
│ ├─ #0 [get_detail] ✓ 80ms → [check_bank] ✓ true → [notify] ✓ │
│ ├─ #1 [get_detail] ✓ 75ms → [check_bank] ✗ false (skipped) │
│ └─ #2 [get_detail] ✓ 90ms → [check_bank] ✗ false (skipped) │
│ │
│ Total: 6 API calls, 1 notification envoyee │
└───────────────────────────────────────────────────────────────────────┘
Comme les actions, chaque operation sur une routine passe par audit.log() :
routine.create, routine.update, routine.deleteroutine.activate, routine.deactivateroutine.execute (avec trigger_type)Quand l'utilisateur construit manuellement une routine dans l'editeur visuel, l'IA l'assiste en temps reel :
L'utilisateur ajoute un node fetch Gmail → le panel de config affiche :
CONNECTOR_TYPES{{nodes. → liste des nodes precedents + leurs outputs connusComme le bouton AI suggest qui existe deja pour les actions SSH, mais pour les nodes :
POST /api/routines/suggest-node-config
Body: {
routine_context: { nodes, edges }, // Les nodes deja places
node_id: "node_3", // Le node a configurer
node_type: "condition",
user_intent: "filtrer les mails de la banque" // Optionnel
}
Response: {
suggested_config: {
operator: "contains",
left: "{{nodes.get_detail.output.payload.headers[0].value}}",
right: "banque"
},
explanation: "Verifie si le champ From du mail contient 'banque'"
}
A chaque modification d'un node, le front envoie le graph au backend pour validation :
{{nodes.inexistant.output}})@xyflow/react (ex React Flow) — MIT, mature, parfait pour les node editors. Deja utilise par n8n, Langflow, etc.
/routines → Liste des routines
/routines/new → Editeur visuel (nouveau)
/routines/:id → Editeur visuel (existant)
/routines/:id/history → Historique d'executions
| Fichier | Contenu |
|---|---|
src/services/routines.ts |
CRUD routines + list executions |
src/services/routineExecutor.ts |
Moteur d'execution (traversee graph, context, expressions) |
src/services/routineExpressions.ts |
Parser/evaluateur d'expressions {{...}} |
src/services/routineScheduler.ts |
Gestionnaire cron (node-cron ou setInterval) |
src/data/routine-templates.ts |
Templates de routines pre-definies |
| Migration SQL | Tables routines + routine_executions |
| Fichier | Contenu |
|---|---|
src/routes/routines/ |
Pages liste + editeur |
src/components/RoutineEditor.tsx |
Editeur node (@xyflow/react) |
src/components/nodes/*.tsx |
Composants visuels par type de node |
src/components/NodeConfigPanel.tsx |
Panel config lateral |
{{...}} dans le panel config