Construire des loops
Une Loop est un workflow taggé origin_product: "loops" avec une forme précise : un trigger, une étape "Create AI task", une étape notify optionnelle, et une sortie. Les écrans Loops de Desktop relisent cette forme. Un workflow avec autre chose s'exécute toujours, mais Desktop l'affiche comme "modifié dans l'éditeur de workflow" et le rend en lecture seule.
Construisez-le dans cet ordre : workflows-create en tant que brouillon, workflows-test-run étape par étape, workflows-schedule-create pour les loops programmées, puis workflows-enable une fois que l'utilisateur a approuvé. Pour tout ce qui n'est pas couvert ici (corriger un brouillon, publier un workflow en direct, lire les logs), utilisez la skill building-workflows.
Le graphe
Remplacez uniquement les valeurs entre chevrons. Omettez l'étape notify et ses arêtes quand l'utilisateur ne veut pas de notification.
{
"name": "<short name>",
"description": "",
"status": "draft",
"origin_product": "loops",
"exit_condition": "exit_only_at_end",
"variables": [{ "key": "task_final_message", "type": "string", "default": "" }],
"actions": [
{ "id": "trigger", "name": "Trigger", "type": "trigger", "config": <trigger config> },
{
"id": "create_task",
"name": "Create AI task",
"type": "function",
"config": {
"template_id": "template-posthog-create-task",
"inputs": {
"prompt": { "value": "<task prompt>" },
"repository": { "value": "<owner/name>" },
"connectors": { "value": ["<mcp connection id>"] },
"skills": { "value": ["<skill name>"] },
"posthog_mcp_scopes": { "value": "read_only" },
"non_failure_status_codes": { "value": [409] }
}
},
"output_variable": [{ "key": "task_final_message", "result_path": "final_message" }]
},
{ "id": "notify", "name": "Notify", "type": "function", "config": <notify config> },
{ "id": "exit", "name": "Exit", "type": "exit", "config": { "reason": "Task finished" } }
],
"edges": [
{ "from": "trigger", "to": "create_task", "type": "continue" },
{ "from": "create_task", "to": "notify", "type": "continue" },
{ "from": "notify", "to": "exit", "type": "continue" }
]
}
L'étape task
prompt est obligatoire. Elle s'exécute sans surveillance, donc écrivez-la comme un brief complet : quoi faire, à quoi ressemble « fait », et quoi rapporter.
Ne mettez jamais de texte d'événement dans le prompt avec un template {event.properties.<name>}. Chaque exécution reçoit déjà l'événement déclencheur entier en tant que bloc <triggering_event> séparé, étiqueté comme données et avec ses chevrons échappés pour que rien à l'intérieur ne puisse forger une balise. Nommez plutôt la propriété et laissez l'exécution la lire là :
Lisez le message depuis la propriété
textde l'événement déclencheur, puis...
Un template rend la valeur brute dans la partie instruction du prompt, avant ce bloc et sans échappement. Un afficheur Slack, un auteur d'issue, ou l'utilisateur final du client écrit cette valeur, donc une valeur construite se lit comme des instructions à un agent qui peut détenir des credentials de repository.
Incluez les autres inputs seulement quand la loop en a besoin :
repository: leowner/nameque l'utilisateur vous donne, quand la task travaille dans le code. Jamais un tiré de la mémoire. Vérifiez qu'il est accessible en premier :integrations-listpour GitHub, puisintegrations-github-repos-retrievepour ce nom exact.connectors: ids depuismcp-connections-list. Seules les connexions partagées avec tout le monde dans le projet sont acceptées.skills: noms exacts depuisskill-list, au maximum 10.posthog_mcp_scopes:read_onlypar défaut. Utilisezfullseulement quand l'utilisateur demande à la task de changer les choses dans PostHog.channel: l'espace auquel la loop appartient, comme<space id>|<space name>. Définissez-le seulement quand l'app vous dit quel espace la loop est créée, jamais à partir d'un nom que l'utilisateur tape. Chaque exécution s'affiche alors dans le feed de cet espace, et la loop est listée sous cet espace.reply_in_slack_thread:true(un booléen JSON, pas une string template) pour une loop déclenchée par Slack dont le résultat doit revenir dans le thread. Cela couvre la notification, donc omettez l'étapenotify.
Conservez non_failure_status_codes exactement comme le graphe l'a, sur chaque loop. L'API répond 409 quand une exécution atteint une limite de task. Sans cet input l'étape échoue avec une erreur fetch générique et l'utilisateur ne lit jamais le message de limite.
Le workflow attend à cette étape jusqu'à ce que la task se termine. L'étape suivante voit alors final_message, pr_urls, et status sur le résultat de l'étape.
Config du trigger
Choisissez le trigger pour la source que l'utilisateur nomme. Schedule et GitHub sont ceux que le formulaire loop de Desktop édite.
Schedule, cadence sur une ligne de schedule (ci-dessous) :
{ "type": "schedule" }
Événement GitHub, un repository, un type d'événement :
{
"type": "internal-event",
"filters": {
"source": "internal-events",
"events": [{ "id": "$github_event_received", "type": "events" }],
"properties": [
{ "key": "repository", "value": ["<owner/name>"], "operator": "exact", "type": "event" },
{
"key": "event_type",
"value": ["<issues | issue_comment | pull_request | push>"],
"operator": "exact",
"type": "event"
},
{ "key": "actor_access", "value": ["write"], "operator": "exact", "type": "event" }
]
}
}
Conservez le filtre actor_access. Il empêche les gens sans accès en écriture au repository de démarrer une task. Réduisez à une action quand l'utilisateur le demande (« quand une issue est ouverte ») avec un filtre de propriété supplémentaire : { "key": "action", "value": ["<action>"], "operator": "exact", "type": "event" }. Une loop pull_request sans filtre action se déclenche à chaque push vers chaque pull request ouverte.
| Événement | Actions |
|---|---|
issues |
opened, reopened, closed, edited, deleted, labeled, unlabeled, assigned, unassigned, pinned, unpinned, transferred |
pull_request |
opened, reopened, closed, synchronize, edited, ready_for_review, converted_to_draft, review_requested, review_request_removed, labeled, unlabeled, assigned, unassigned |
issue_comment |
created, edited, deleted |
push |
aucune, push n'a pas d'actions |
Message Slack dans un ou plusieurs canaux. channel est obligatoire et prend des IDs de canal. Résolvez un nom en son id avec integrations-channels-retrieve pour l'intégration Slack du projet (integrations-list, kind slack). Ne devinez jamais un id :
{
"type": "internal-event",
"filters": {
"source": "internal-events",
"events": [{ "id": "$slack_message_received", "type": "events" }],
"properties": [
{ "key": "channel", "value": ["<channel id>"], "operator": "exact", "type": "event" },
{ "key": "bot_id", "value": "is_not_set", "operator": "is_not_set", "type": "event" },
{ "key": "thread_ts", "value": "is_not_set", "operator": "is_not_set", "type": "event" }
]
}
}
Conservez les filtres bot_id et thread_ts. Ils maintiennent la loop aux messages de haut niveau qu'une personne a tapés, ce que le trigger Slack de l'éditeur de workflow crée. Sans eux la loop démarre une task à chaque alerte qu'une autre app publie et à chaque réponse sous n'importe quel thread, et chacune dépense le budget quotidien de task. Élargissez seulement quand l'utilisateur le demande : bot_id avec is_set pour les apps et bots seulement, un filtre exact sur user ou app_id pour les afficheurs nommés, et supprimez le filtre thread_ts pour inclure les réponses.
Événement PostHog, chaque occurrence correspondante :
{
"type": "event",
"filters": {
"events": [{ "id": "<event>", "name": "<event>", "type": "events", "order": 0, "properties": [] }],
"properties": [],
"filter_test_accounts": false
}
}
Un trigger d'événement PostHog crée une task à chaque occurrence correspondante, et l'avertissement de volume de l'éditeur de workflow ne s'exécute pas sur ce chemin. Limitez-le avant de créer la loop : réduisez l'événement avec un filtre de propriété, et régulièz un événement courant avec trigger_masking, un champ de haut niveau à côté de actions et edges.
"trigger_masking": { "hash": "{person.id}", "ttl": 3600, "threshold": null }
hash est un template HogQL pour la clé de dédup, donc {person.id} se déclenche une fois par personne. ttl est combien de temps pour supprimer les répétitions de ce hash, en secondes, de 60 à environ trois ans. threshold se déclenche une fois pour N correspondances du même hash à la place ; omettez-le pour une simple dédup. N'envoyez jamais bytecode ; le serveur le compile depuis hash. Un workflow peut créer 100 tasks par jour et un projet 500 sur tous ses workflows, donc une loop sans limite sur un événement chargé dépense le budget du projet et le workflow suivant à se déclencher est refusé. Éditer la loop dans Desktop conserve trigger_masking.
Manual, exécuter depuis le bouton "déclencher manuellement" :
{
"type": "manual",
"template_id": "template-source-webhook",
"inputs": { "event": { "value": "$workflow_triggered" }, "distinct_id": { "value": "{request.body.user_id}" } }
}
Ligne de schedule
Créez-la avec workflows-schedule-create après que le workflow existe, avec l'id du workflow comme workflow_id. rrule est exactement l'un de ceux-ci. Pas de BYHOUR ou BYMINUTE.
| Cadence | rrule |
|---|---|
| Toutes les heures | FREQ=HOURLY;INTERVAL=1 |
| Tous les jours | FREQ=DAILY;INTERVAL=1 |
| Jours ouvrables | FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR |
| Un jour par semaine | FREQ=WEEKLY;INTERVAL=1;BYDAY=<MO..SU> |
| Une fois | FREQ=DAILY;COUNT=1 |
starts_at est la première exécution, en ISO 8601 avec un décalage UTC. L'heure de l'horloge en provient, donc choisissez l'occurrence suivante à l'heure que l'utilisateur veut. timezone est le fuseau horaire IANA de l'utilisateur. Les schedules horaires commencent à l'heure.
Étape notify
Le résultat de la task atteint l'étape notify via l'output_variable sur l'étape task : {variables.task_final_message} est le message de fermeture de l'agent. Ajoutez { "key": "task_pr_urls", "result_path": "pr_urls" } à la liste, et une entrée variables correspondante, quand le message doit lier le pull request.
Slack, template_id template-slack. slack_workspace est l'id d'intégration Slack depuis integrations-list. Résolvez channel avec integrations-channels-retrieve et lisez le is_member de ce canal avant de résumer. False signifie que l'app Slack de PostHog n'est pas dans le canal : la première vraie exécution échoue à poster et le résultat n'atteint personne, donc demandez à l'utilisateur d'inviter l'app plutôt que de construire la loop :
{
"template_id": "template-slack",
"inputs": {
"slack_workspace": { "value": <integration id> },
"channel": { "value": "<channel id>" },
"text": { "value": "<loop name> finished: {variables.task_final_message}" }
}
}
Email : définissez le type de l'étape à function_email au lieu de function. from est un sender id depuis integrations-list, et to est un objet contenant l'adresse de l'utilisateur. Envoyez html en tant que string vide : le runtime demande la clé, et une vide envoie un email texte seul. L'input email accepte Liquid, donc utilisez {{ variables.task_final_message }} ici :
{
"template_id": "template-email",
"inputs": {
"email": {
"value": {
"from": { "integrationId": <sender id> },
"to": { "email": "<user email>", "name": "" },
"subject": "<loop name> finished",
"text": "{{ variables.task_final_message }}",
"html": ""
}
}
}
}
Sortie structurée
Quand l'étape notify ou l'utilisateur a besoin d'un champ spécifique de la task, tel qu'un verdict ou un décompte, demandez-le à la task au lieu d'analyser la prose. Ajoutez une variable de sortie par champ avec result_path output.<name> et une entrée variables correspondante avec le type (string, number, boolean). La task est informée des champs à retourner, et {variables.<name>} contient la valeur après. Omettez ceci quand final_message suffit.
Test run
Testez le brouillon avant de le programmer ou de l'activer. workflows-test-run exécute une étape à la fois et mock chaque appel sortant, donc rien de réel n'est créé. Cela couvre l'étape notify ainsi que la task, donc un test run réussi dit que le graphe est câblé, pas que la livraison Slack ou email marche.
- Exécutez sans
current_action_idetglobals{ "event": { "event": "$scheduled", "properties": {} } }pour une loop schedule. Pour une loop GitHub ou Slack, envoyez le nom d'événement du trigger avec les propriétés qui correspondent aux filtres, et pas de personne. Pour une loop d'événement PostHog, envoyez cet événement avec une personne. - Attendez
nextActionId=create_task. Exécutez à nouveau aveccurrent_action_id: "create_task". - Attendez l'appel task mocké et
nextActionId=notify(ouexit). Continuez jusqu'à l'étape exit.
Un status=skipped à l'étape 1 signifie que l'événement d'exemple ne correspond pas aux filtres du trigger. Corrigez l'exemple, pas le trigger.
Non disponible dans Loops
Notifications in-app ou push, et tout autre type d'étape. Si l'utilisateur demande l'une d'elle, dites que Loops ne la supporte pas encore et proposez la loop la plus proche qui convient. N'ajoutez pas d'actions, d'arêtes, ou d'inputs pour contourner cela.