Skip to main content

building-loops

Build a Loop for PostHog Desktop: a workflow that creates an AI task each time its trigger fires, optionally followed by a Slack or email notification with the task's result. Use when asked to create, set up, or change a loop, a recurring agent, a scheduled task, or an automation that runs an AI task when a GitHub, Slack, or PostHog event happens. Covers the exact graph, trigger configs, schedule presets, the notify step, structured output, and the test-run steps.

Datos de origen

Repositorio
PostHog/ai-plugin
Última actividad en el origen
25 de septiembre de 2026 a las 08:37
Idioma detectado de SKILL.md
inglés
Estrellas
95
Forks
20

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
building-loops
description
Build a Loop for PostHog Desktop: a workflow that creates an AI task each time its trigger fires, optionally followed by a Slack or email notification with the task's result. Use when asked to create, set up, or change a loop, a recurring agent, a scheduled task, or an automation that runs an AI task when a GitHub, Slack, or PostHog event happens. Covers the exact graph, trigger configs, schedule presets, the notify step, structured output, and the test-run steps.
# Building loops A Loop is a workflow tagged `origin_product: "loops"` with one shape: a trigger, one "Create AI task" step, an optional notify step, and an exit. Desktop's Loops screens read this shape back. A workflow with anything else still runs, but Desktop shows it as "changed in the workflow editor" and makes it read-only. Build it in this order: `workflows-create` as a draft, `workflows-test-run` step by step, `workflows-schedule-create` for schedule loops, then `workflows-enable` once the user signs off. For anything not covered here (patching a draft, publishing a live workflow, reading logs), use the `building-workflows` skill. ## The graph Replace only the values in angle brackets. Leave out the `notify` step and its edges when the user does not want a notification. ```json { "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" } ] } ``` ## The task step `prompt` is required. It runs unattended, so write it as a complete brief: what to do, what "done" looks like, and what to report. Never put event text in the prompt with a `{event.properties.<name>}` template. Every run already receives the whole triggering event as a separate `<triggering_event>` block, labelled as data and with its angle brackets escaped so nothing inside it can forge a tag. Name the property instead and let the run read it there: > Read the message from the `text` property of the triggering event, then... A template renders the raw value into the instruction part of the prompt, before that block and with no escaping. A Slack poster, an issue author, or a customer's own end user writes that value, so a crafted one reads as instructions to an agent that may hold repository credentials. Include the other inputs only when the loop needs them: - `repository`: the `owner/name` the user gives you, when the task works in code. Never one from memory. Check it is reachable first: `integrations-list` for GitHub, then `integrations-github-repos-retrieve` for that exact name. - `connectors`: ids from `mcp-connections-list`. Only connections shared with everyone in the project are accepted. - `skills`: exact names from `skill-list`, at most 10. - `posthog_mcp_scopes`: `read_only` by default. Use `full` only when the user asks for the task to change things in PostHog. - `channel`: the space the loop belongs to, as `<space id>|<space name>`. Set it only when the app tells you which space the loop is being created in, never from a name the user types. Each run then shows up in that space's feed, and the loop is listed under that space. - `reply_in_slack_thread`: `true` (a JSON boolean, not a template string) for a Slack-triggered loop whose result should land back in the thread. That covers the notification, so leave out the `notify` step. Keep `non_failure_status_codes` exactly as the graph has it, on every loop. The API answers 409 when a run hits a task limit. Without this input the step fails with a generic fetch error and the user never reads the limit message. The workflow waits at this step until the task finishes. The next step then sees `final_message`, `pr_urls`, and `status` on the step result. ## Trigger config Pick the trigger for the source the user names. Schedule and GitHub are the ones Desktop's loop form edits. Schedule, cadence on a schedule row (below): ```json { "type": "schedule" } ``` GitHub event, one repository, one event type: ```json { "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" } ] } } ``` Keep the `actor_access` filter. It stops people without write access to the repository from starting a task. Narrow to one action when the user asks ("when an issue is opened") with one more property filter: `{ "key": "action", "value": ["<action>"], "operator": "exact", "type": "event" }`. A `pull_request` loop without an `action` filter fires on every push to every open pull request. | Event | 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` | none, `push` has no actions | Slack message in one or more channels. `channel` is required and takes channel IDs. Resolve a name to its id with `integrations-channels-retrieve` for the project's Slack integration (`integrations-list`, kind `slack`). Never guess an id: ```json { "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" } ] } } ``` Keep the `bot_id` and `thread_ts` filters. They hold the loop to top-level messages a person typed, which is what the workflow editor's Slack trigger creates. Without them the loop starts a task on every alert another app posts and on every reply under any thread, and each one spends the daily task budget. Widen only when the user asks: `bot_id` with `is_set` for apps and bots only, an `exact` filter on `user` or `app_id` for named posters, and drop the `thread_ts` filter to include replies. PostHog event, every matching occurrence: ```json { "type": "event", "filters": { "events": [{ "id": "<event>", "name": "<event>", "type": "events", "order": 0, "properties": [] }], "properties": [], "filter_test_accounts": false } } ``` A PostHog event trigger creates a task on every matching occurrence, and the workflow editor's volume warning does not run on this path. Bound it before you create the loop: narrow the event with a property filter, and throttle a common event with `trigger_masking`, a top-level field beside `actions` and `edges`. ```json "trigger_masking": { "hash": "{person.id}", "ttl": 3600, "threshold": null } ``` `hash` is a HogQL template for the dedup key, so `{person.id}` fires once per person. `ttl` is how long to suppress repeats of that hash, in seconds, from 60 to about three years. `threshold` fires once per N matches of the same hash instead; leave it out for plain dedup. Never send `bytecode`; the server compiles it from `hash`. A workflow can create 100 tasks a day and a project 500 across all its workflows, so an unbounded loop on a busy event spends the project's budget and the next workflow to fire is refused. Editing the loop in Desktop keeps `trigger_masking`. Manual, run from the "trigger manually" button: ```json { "type": "manual", "template_id": "template-source-webhook", "inputs": { "event": { "value": "$workflow_triggered" }, "distinct_id": { "value": "{request.body.user_id}" } } } ``` ## Schedule row Create it with `workflows-schedule-create` after the workflow exists, with the workflow id as `workflow_id`. `rrule` is one of these, exactly. No `BYHOUR` or `BYMINUTE`. | Cadence | rrule | | -------------- | --------------------------------------------- | | Every hour | `FREQ=HOURLY;INTERVAL=1` | | Every day | `FREQ=DAILY;INTERVAL=1` | | Weekdays | `FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR` | | One day a week | `FREQ=WEEKLY;INTERVAL=1;BYDAY=<MO..SU>` | | Once | `FREQ=DAILY;COUNT=1` | `starts_at` is the first run, as ISO 8601 with a UTC offset. The clock time comes from it, so pick the next occurrence at the time the user wants. `timezone` is the user's IANA timezone. Hourly schedules start on the hour. ## Notify step The task's result reaches the notify step through the `output_variable` on the task step: `{variables.task_final_message}` is the agent's closing message. Add `{ "key": "task_pr_urls", "result_path": "pr_urls" }` to the list, and a matching `variables` entry, when the message should link the pull request. Slack, `template_id` `template-slack`. `slack_workspace` is the Slack integration id from `integrations-list`. Resolve `channel` with `integrations-channels-retrieve` and read that channel's `is_member` before you summarize. False means the PostHog Slack app is not in the channel: the first real run fails to post and the result reaches nobody, so ask the user to invite the app rather than building the loop: ```json { "template_id": "template-slack", "inputs": { "slack_workspace": { "value": <integration id> }, "channel": { "value": "<channel id>" },
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub