Skip to content

feat(desktop): Implement loops ➿ - #70604

Merged
charlesvien merged 105 commits into
masterfrom
feat/loops
Jul 21, 2026
Merged

feat(desktop): Implement loops ➿#70604
charlesvien merged 105 commits into
masterfrom
feat/loops

Conversation

@charlesvien

@charlesvien charlesvien commented Jul 14, 2026

Copy link
Copy Markdown
Member

Problem

Loops put recurring PostHog work on autopilot. Instead of kicking off the same agent task by hand every time, you define a loop once and it runs itself on PostHog's managed cloud infrastructure: on a schedule, when a GitHub event fires, or when your own code calls its API. This PR adds the backend API to create and manage them. The desktop UI lives in the code repo [companion PR].

Changes

Loops are named, cloud-executed agent automations: a prompt + model + triggers, running on the existing tasks pipeline. This PR is the backend and MCP surface; the UI ships separately in PostHog/code. Spec and pressure-test docs are in products/tasks/docs/.

  • New Loop, LoopTrigger and LoopFire (per-fire dedup) models, plus a nullable loop FK on Task. Triggers: cron or one-time schedules, GitHub webhook events (pull_request, issues, issue_comment, push) and an external API trigger
  • Every trigger path funnels through one fire_loop() that applies dedup, daily rate caps (100 fires/loop, 500/team) and the loop's overlap policy (skip, allow or cancel previous) under a per-team advisory lock, then creates an internal Task + TaskRun on the standard tasks pipeline
  • Endpoints under loops/: CRUD plus run/ (manual fire), trigger/ (external fire, callable with a project secret API key via the new loop:write scope, the first write-capable PSAK scope), runs/ history and a dry-run preview/
  • The GitHub webhook in posthog/urls.py becomes a per-event-type handler registry with per-handler Redis dedup (released if a handler fails so redeliveries retry); conversations, tasks and installation handlers keep their slots, loops register alongside
  • Contexts: a loop can attach to a desktop context folder and file runs into its feed, maintain its context.md and/or rewrite a canvas dashboard; context-attached loops must be team-visible
  • Guardrails: 100 loops/team, 25 triggers/loop and 1 repo/loop (structured 429), auto-pause after 5 consecutive failures, owner deactivation or GitHub App uninstall pauses affected loops and cancels their in-flight runs (cause recorded in disabled_reason), retention sweeps (200 tasks kept per loop, fire records pruned after 7 days)
  • Team loops need an explicit ownership takeover to edit, re-validating repositories and connectors against the new owner's GitHub and MCP access; config changes, takeovers and manual fires are activity-logged, and personal loop activity stays hidden from other members
  • Notifications per loop: in-app always, push/email/Slack opt-in with per-channel event filters, a failure cooldown and Slack auto-disable on permanent errors
  • loops-* MCP tools plus a loops-review card (MCP UI app): an agent assembles a loop from natural language and the user confirms with a Create button instead of it being created blind

Temporal changes

  • One Temporal Schedule per schedule trigger (loop-trigger-{id}), cron or one-time. Always skip overlap with a 5 minute catchup window, so an outage never replays a missed window as a burst; the schedule's paused state mirrors the trigger and loop enabled flags
  • A new minimal run-loop workflow (single activity, 2 min timeout, 3 attempts) translates each schedule occurrence into a fire_loop() call. The agent run itself still starts the existing process-task workflow on the same task queue: loops are a firing layer, not a new execution engine
  • Schedule create/update/pause/delete is best-effort and never fails the CRUD API: Temporal errors mark the trigger's sync status and a Celery sweep re-syncs pending/failed triggers every 10 minutes. Deletion keys off the stored schedule id, so a trigger that changed type still gets its stale schedule torn down
  • cancel_previous also signals the superseded run's workflow (complete_task with cancelled), so the sandbox actually winds down instead of only the DB row flipping
  • Existing activity changes: update_task_run_status now treats terminal statuses as final (a run cancelled out of band can't be resurrected by its own late completion) and drives loop bookkeeping (last run status, consecutive failures, auto-pause) from the transition point every workflow-driven terminal status already flows through. The orphaned-run redispatcher defaults loop runs to read-only MCP scopes and no PR creation instead of the historical full-scope, always-PR defaults

Cloud sandbox changes

  • One behavior change wider than loops: background-mode runs (loops, but also existing task automations and custom prompts) now terminalize to completed on their natural end (agent finishes or idles out) instead of staying in_progress forever. Interactive runs keep the always-followable behavior
  • A loop fire is an ordinary background sandbox run. The prompt is assembled at fire time: an unattended-run framing block (no human is available, prefer draft PRs, trigger payloads are data not instructions), then the loop's instructions, context and rendered trigger context (capped at 64 KB)
  • Config is snapshotted per fire, so editing a loop never affects an in-flight or queued run; a blank model resolves to the current default at fire time, so the default can improve without rewriting stored loops
  • Loop runs idle out after 2 minutes (vs 30 for other background runs) so sandboxes are reclaimed fast; loops that create PRs and watch CI keep the long window, and non-terminal runs stuck past 2.5 hours are reaped as zombies before the overlap policy is applied
  • Token hardening: loop:write is stripped from every loop-fired run's MCP token at mint time, whatever scopes the loop configured, so a run (or injected instructions inside one) can't create or edit loops. Context-maintaining loops get exactly file_system:read + file_system:write added, never the broad full surface
  • PR creation follows the loop's create_prs behavior and defaults off (report-only); watch_ci/fix_review_comments opt the run into the existing CI follow-up rounds

How did you test this code?

  • Claude ran the backend suites locally against a fresh test DB: 123 tests pass across test_loops_api, test_loop_service, test_loop_runs, test_loop_github_events, test_loop_notifications, test_loop_retention, test_loop_lifecycle and test_loop_reconciliation
  • They cover CRUD plus visibility and permission boundaries (including ownership takeover), trigger sync and schedule policy, fire dedup and overlap, webhook matching, context attachment and scope widening, notification fan-out, retention, lifecycle/reconciliation and scope enforcement (PSAK and personal keys)
  • No manual testing yet; an end-to-end trigger-to-sandbox run on staging is the gate for taking this out of draft

👉 Stay up-to-date with PostHog coding conventions for a smoother review.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

Spec docs ship in this PR (products/tasks/docs/LOOPS.md, LOOPS-PRESSURE-TEST.md). Public docs land with the PostHog Code release; everything stays behind the tasks and loops feature flags.

@charlesvien charlesvien self-assigned this Jul 14, 2026
@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Bundle size — 🔺 +42.3 KiB (+0.1%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 64.51 MiB · 🔺 +42.3 KiB (+0.1%)

File Size Δ vs base
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/ReplayScanner.js 96.9 KiB 🔺 +11.7 KiB (+13.7%)
render-query/src/render-query/render-query.js 21.29 MiB 🔺 +11.4 KiB (+0.1%)
posthog-app/src/scenes/App.js 32.1 KiB 🔺 +10.2 KiB (+46.2%)
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/ScannerEditorScene.js 23.6 KiB 🟢 -7.5 KiB (-24.1%)
posthog-app/src/scenes/hog-functions/HogFunctionScene.js 51.3 KiB 🔺 +4.3 KiB (+9.1%)
posthog-app/_parent/products/mcp_analytics/frontend/MCPAnalyticsScene.js 140.4 KiB 🔺 +2.7 KiB (+2.0%)
exporter/src/exporter/scenes/ExporterDashboardScene.js 279.0 KiB 🔺 +2.5 KiB (+0.9%)
posthog-app/src/scenes/inbox/InboxScene.js 677.8 KiB 🔺 +2.0 KiB (+0.3%)
posthog-app/_parent/products/ai_observability/frontend/evaluations/EvaluationTemplates.js 2.9 KiB 🔺 +2.0 KiB (+216.3%)
posthog-app/_parent/products/ai_observability/frontend/prompts/LLMPromptScene.js 41.0 KiB 🔺 +1.5 KiB (+3.9%)

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.24 MiB · 22 files 🔺 +8.8 KiB (+0.7%) ███░░░░░░░ 28.8% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.19 MiB · 3,001 files 🔺 +3.9 KiB (+0.0%) █████████░ 88.5% of 9.25 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
281.3 KiB ../node_modules/.pnpm/posthog-js@1.405.2/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
235.8 KiB src/taxonomy/core-filter-definitions-by-group.json
224.4 KiB ../node_modules/.pnpm/posthog-js@1.405.2/node_modules/posthog-js/dist/module.js
167.1 KiB src/queries/validators.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
106.0 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
93.2 KiB ../packages/quill/packages/quill/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Dist folder size — 🔺 +2.24 MiB (+0.2%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1355.36 MiB · 🔺 +2.24 MiB (+0.2%)

ℹ️ MCP UI apps size — 32 app(s), 17063.6 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 599.5 KB 187.7 KB
action 457.8 KB 187.7 KB
action-list 564.3 KB 187.7 KB
cohort 456.8 KB 187.7 KB
cohort-list 563.3 KB 187.7 KB
email-template 456.6 KB 187.7 KB
error-details 472.4 KB 187.7 KB
error-issue 457.5 KB 187.7 KB
error-issue-list 564.2 KB 187.7 KB
experiment 561.5 KB 187.7 KB
experiment-list 565.1 KB 187.7 KB
experiment-results 563.2 KB 187.7 KB
feature-flag 567.1 KB 187.7 KB
feature-flag-list 570.9 KB 187.7 KB
feature-flag-testing 461.0 KB 187.7 KB
insight-actors 562.1 KB 187.7 KB
invite-email-preview 456.0 KB 187.7 KB
llm-costs 559.5 KB 187.7 KB
session-recording 458.6 KB 187.7 KB
session-summary 463.9 KB 187.7 KB
survey 458.4 KB 187.7 KB
survey-global-stats 562.2 KB 187.7 KB
survey-list 565.0 KB 187.7 KB
survey-stats 562.2 KB 187.7 KB
trace-span 457.2 KB 187.7 KB
trace-span-list 564.2 KB 187.7 KB
workflow 457.1 KB 187.7 KB
workflow-list 563.7 KB 187.7 KB
loops-review 459.3 KB 187.7 KB
query-results 745.5 KB 187.7 KB
render-ui 826.2 KB 187.7 KB
visual-review-snapshots 461.6 KB 187.7 KB
Playwright — all passed

All tests passed.

View test results →

⚠️ Backend coverage — 86.0% of changed backend lines covered — 538 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): █████████████████░░░ 86.0% (3,417 / 3,955)

File Patch Uncovered changed lines
products/tasks/backend/temporal/execute_sandbox/workflow.py 0.0% 1318, 1327–1328
products/tasks/backend/temporal/process_task/tests/test_send_followup_to_sandbox.py 0.0% 95
products/tasks/backend/temporal/process_task/tests/test_utils.py 0.0% 368–370, 373–374, 379, 381, 383–387, 389, 391, 402–403
products/tasks/backend/temporal/process_task/tests/test_sandbox_credentials.py 0.0% 325–327, 329–330, 332–333, 335–341, 343, 345–346, 348–349, 351, 353–356, 359–364, 366, 368–369, 519–521, 524, 530–532, 535, 544
products/tasks/backend/temporal/process_task/activities/tests/test_update_task_run_status.py 0.0% 6, 41–42, 45–46, 48–49, 51–52, 174–175, 180, 187–189, 191, 194–195, 197–201
products/tasks/backend/temporal/process_task/activities/update_task_run_status.py 8.3% 50–51, 57, 61–62, 68, 70–73, 75, 78–80, 85–86, 103–104, 108–111
products/tasks/backend/temporal/process_task/utils.py 15.2% 206–209, 455–460, 497–499, 956, 966–970, 974–975, 1020–1026
products/tasks/backend/temporal/process_task/sandbox_credentials.py 15.8% 128–133, 137, 152, 167–168, 268–269, 355–356, 365–366
products/tasks/backend/logic/services/run_actor.py 22.2% 35–39, 42, 45
products/tasks/backend/temporal/oauth.py 26.7% 53–54, 78, 103–105, 118–121, 126
products/tasks/backend/temporal/process_task/activities/get_task_processing_context.py 33.3% 131–132, 549–552
products/tasks/backend/loop_service.py 48.9% 40, 42, 117, 133–135, 137, 139–140, 144, 165–166, 190, 195–207, 217, 222–234, 246, 249–251, 256–257, 268, 270, 274–277, 280–281, 286, 291–301, 303–305
posthog/urls.py 50.0% 122, 124, 130, 132, 144, 146–147, 187–191, 194, 198, 205–208, 256–257, 261–262, 265–268
products/tasks/backend/temporal/client.py 62.5% 100–101, 371
products/tasks/backend/admin.py 64.9% 186, 191–192, 195–197, 211, 216, 220–221, 224–226
products/tasks/backend/loop_lifecycle.py 66.7% 48–49, 57–58, 77–78, 82–83, 153–155, 163, 166, 171–172, 176, 178–184, 196, 200–201
posthog/api/github_callback/installation_events.py 72.7% 76–78
products/tasks/backend/temporal/loops/activities.py 73.1% 13–15, 29–30, 43–44
products/tasks/backend/loop_retention.py 73.2% 42, 64, 93–95, 103–112
products/tasks/backend/presentation/serializers_loops.py 74.6% 43–47, 137, 140–141, 155, 162, 165, 171, 177–181, 183–191, 195–200, 209–211, 214–219, 222–225, 227, 237, 274, 389, 393–397, 401, 405, 413, 425, 428–429, 434–437, 445, 448
products/tasks/backend/loop_notifications.py 83.3% 93, 95, 98, 129–130, 139, 141, 144–145, 154, 170–171, 183, 187, 190, 207–208, 220, 238–239, 245
products/tasks/backend/temporal/loops/workflow.py 84.6% 15, 19
products/tasks/backend/loop_github_events.py 85.9% 60–62, 132–136, 160–163, 168, 170, 185–187, 255, 263, 275, 286–288, 326
products/tasks/backend/loop_reconciliation.py 88.9% 55–56
products/tasks/backend/facade/loops.py 90.5% 304, 373–379, 436–437, 477, 484, 487, 503, 538, 547–549, 585–587, 601, 616, 633, 644, 737, 759, 773–780, 786–787, 805, 970, 990, 1022, 1030–1031, 1061–1063, 1075, 1095, 1111, 1153–1154, 1190
products/tasks/backend/activity_logging.py 92.9% 22
products/tasks/backend/presentation/views/loops.py 93.7% 57–58, 65, 72, 113, 124–125, 195, 252, 299, 336
posthog/api/advanced_activity_logs/viewset.py 94.4% 81
products/tasks/backend/models.py 94.8% 1168, 1176, 1223, 1231–1232, 1303
products/tasks/backend/logic/services/loop_runs.py 95.7% 138, 143, 199–200, 237, 276, 343, 349, 454, 590, 602, 606
products/tasks/backend/tests/test_loop_github_events.py 99.2% 109
products/tasks/backend/tests/test_loops_api.py 99.6% 19–20

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29875359191 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.6% 8,414 / 21,244
demo ███████████░░░░░░░░░ 56.2% 1,497 / 2,663
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
tasks ██████████████░░░░░░ 68.2% 29,902 / 43,831
data_tools ██████████████░░░░░░ 70.0% 63 / 90
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 78.9% 20,140 / 25,521
cdp ████████████████░░░░ 80.7% 3,118 / 3,864
data_modeling █████████████████░░░ 82.8% 5,529 / 6,675
wizard █████████████████░░░ 84.8% 935 / 1,102
notebooks █████████████████░░░ 85.3% 7,266 / 8,522
agent_platform █████████████████░░░ 86.4% 3,807 / 4,405
actions █████████████████░░░ 86.6% 717 / 828
cohorts █████████████████░░░ 87.0% 4,303 / 4,945
product_tours ██████████████████░░ 87.9% 1,303 / 1,482
exports ██████████████████░░ 88.4% 6,943 / 7,853
engineering_analytics ██████████████████░░ 88.9% 5,507 / 6,196
business_knowledge ██████████████████░░ 89.0% 4,391 / 4,936
conversations ██████████████████░░ 89.2% 16,508 / 18,509
visual_review ██████████████████░░ 89.4% 5,818 / 6,505
dashboards ██████████████████░░ 89.5% 5,988 / 6,692
mcp_analytics ██████████████████░░ 89.7% 2,663 / 2,969
alerts ██████████████████░░ 89.9% 4,054 / 4,508
error_tracking ██████████████████░░ 90.1% 9,761 / 10,832
early_access_features ██████████████████░░ 90.1% 1,031 / 1,144
streamlit_apps ██████████████████░░ 90.4% 2,501 / 2,767
slack_app ██████████████████░░ 90.6% 8,989 / 9,926
links ██████████████████░░ 90.6% 183 / 202
marketing_analytics ██████████████████░░ 91.0% 11,792 / 12,964
stamphog ██████████████████░░ 91.0% 3,993 / 4,387
product_analytics ██████████████████░░ 91.3% 5,804 / 6,355
mcp_store ██████████████████░░ 91.9% 4,257 / 4,634
data_warehouse ███████████████████░ 92.6% 19,249 / 20,790
ai_observability ███████████████████░ 92.7% 14,758 / 15,926
notifications ███████████████████░ 92.7% 1,031 / 1,112
web_analytics ███████████████████░ 92.9% 13,890 / 14,950
workflows ███████████████████░ 92.9% 5,648 / 6,077
surveys ███████████████████░ 93.0% 5,730 / 6,163
posthog_ai ███████████████████░ 93.2% 1,325 / 1,421
approvals ███████████████████░ 93.3% 3,395 / 3,640
reminders ███████████████████░ 93.4% 468 / 501
managed_migrations ███████████████████░ 93.8% 1,220 / 1,300
legal_documents ███████████████████░ 94.1% 1,568 / 1,667
endpoints ███████████████████░ 94.1% 8,640 / 9,177
messaging ███████████████████░ 94.3% 2,682 / 2,845
revenue_analytics ███████████████████░ 94.5% 3,598 / 3,809
tracing ███████████████████░ 94.5% 2,670 / 2,826
skills ███████████████████░ 94.5% 2,893 / 3,061
review_hog ███████████████████░ 94.6% 6,808 / 7,193
logs ███████████████████░ 95.4% 9,935 / 10,416
experiments ███████████████████░ 95.7% 24,469 / 25,569
replay_vision ███████████████████░ 96.1% 14,667 / 15,268
growth ███████████████████░ 96.1% 3,233 / 3,364
annotations ███████████████████░ 96.2% 732 / 761
feature_flags ███████████████████░ 96.4% 17,027 / 17,654
user_interviews ███████████████████░ 96.5% 2,639 / 2,735
warehouse_sources ███████████████████░ 96.8% 308,122 / 318,347
access_control ███████████████████░ 96.8% 851 / 879
customer_analytics ███████████████████░ 97.3% 7,780 / 7,999
data_catalog ███████████████████░ 97.4% 2,345 / 2,407
analytics_platform ████████████████████ 98.0% 2,102 / 2,145
metrics ████████████████████ 98.2% 2,491 / 2,536
pulse ████████████████████ 98.4% 2,017 / 2,049
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

⚠️ MCP snapshots — 5 updated (2 modified, 3 added, 0 deleted)

Snapshots: MCP unit test snapshots updated

Changes: 5 snapshots (2 modified, 3 added, 0 deleted)

What this means:

  • Snapshots have been automatically updated to match current output

Next steps:

  • Review the changes to ensure they're intentional
  • If unexpected, investigate what caused the output to change

Review snapshot changes →

⚠️ Django migration SQL — 6 new migrations to review

We've detected new migrations on this PR. Review the SQL output for each migration:

products/tasks/backend/migrations/0063_loop_looptrigger_loopfire.py

BEGIN;
--
-- Create model Loop
--
CREATE TABLE "posthog_task_loop" ("id" uuid NOT NULL PRIMARY KEY, "name" varchar(400) NOT NULL, "description" text NOT NULL, "visibility" varchar(16) NOT NULL, "instructions" text NOT NULL, "runtime_adapter" varchar(32) NOT NULL, "model" varchar(128) NOT NULL, "reasoning_effort" varchar(32) NULL, "repositories" jsonb NOT NULL, "enabled" boolean NOT NULL, "overlap_policy" varchar(32) NOT NULL, "behaviors" jsonb NOT NULL, "connectors" jsonb NOT NULL, "notifications" jsonb NOT NULL, "context_target" jsonb NOT NULL, "internal" boolean NOT NULL, "origin_product" varchar(32) NOT NULL, "last_run_at" timestamp with time zone NULL, "last_run_status" varchar(32) NULL, "last_error" text NULL, "consecutive_failures" integer NOT NULL CHECK ("consecutive_failures" >= 0), "disabled_reason" varchar(64) NULL, "deleted" boolean NOT NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "created_by_id" integer NULL, "sandbox_environment_id" uuid NULL, "team_id" integer NOT NULL);
--
-- Create model LoopTrigger
--
CREATE TABLE "posthog_task_loop_trigger" ("id" uuid NOT NULL PRIMARY KEY, "type" varchar(16) NOT NULL, "enabled" boolean NOT NULL, "config" jsonb NOT NULL, "github_integration_id" bigint NULL, "repository" varchar(512) NULL, "event_types" varchar(32)[] NULL, "schedule_sync_status" varchar(16) NULL, "last_fired_at" timestamp with time zone NULL, "created_at" timestamp with time zone NOT NULL, "updated_at" timestamp with time zone NOT NULL, "loop_id" uuid NOT NULL, "team_id" integer NOT NULL);
--
-- Create model LoopFire
--
CREATE TABLE "posthog_task_loop_fire" ("id" uuid NOT NULL PRIMARY KEY, "fire_key" varchar(512) NOT NULL, "outcome_reason" varchar(64) NULL, "outcome_task_id" uuid NULL, "outcome_task_run_id" uuid NULL, "created_at" timestamp with time zone NOT NULL, "loop_id" uuid NULL, "team_id" integer NOT NULL, "loop_trigger_id" uuid NULL);
--
-- Alter field origin_product on task
--
-- (no-op)
--
-- Add field loop to task
--
ALTER TABLE "posthog_task" ADD COLUMN "loop_id" uuid NULL CONSTRAINT "posthog_task_loop_id_40d866e1_fk_posthog_task_loop_id" REFERENCES "posthog_task_loop"("id") DEFERRABLE INITIALLY DEFERRED; SET CONSTRAINTS "posthog_task_loop_id_40d866e1_fk_posthog_task_loop_id" IMMEDIATE;
ALTER TABLE "posthog_task_loop" ADD CONSTRAINT "posthog_task_loop_sandbox_environment__23bf7f89_fk_posthog_s" FOREIGN KEY ("sandbox_environment_id") REFERENCES "posthog_sandbox_environment" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "posthog_task_loop_created_by_id_31bb8718" ON "posthog_task_loop" ("created_by_id");
CREATE INDEX "posthog_task_loop_sandbox_environment_id_23bf7f89" ON "posthog_task_loop" ("sandbox_environment_id");
CREATE INDEX "posthog_task_loop_team_id_9933d2cf" ON "posthog_task_loop" ("team_id");
ALTER TABLE "posthog_task_loop_trigger" ADD CONSTRAINT "posthog_task_loop_tr_loop_id_55782e35_fk_posthog_t" FOREIGN KEY ("loop_id") REFERENCES "posthog_task_loop" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "posthog_task_loop_trigger_loop_id_55782e35" ON "posthog_task_loop_trigger" ("loop_id");
CREATE INDEX "posthog_task_loop_trigger_team_id_0d7d32e3" ON "posthog_task_loop_trigger" ("team_id");
CREATE INDEX "task_loop_trigger_gh_repo_idx" ON "posthog_task_loop_trigger" ("github_integration_id", "repository");
ALTER TABLE "posthog_task_loop_fire" ADD CONSTRAINT "posthog_task_loop_fi_loop_trigger_id_09f029d3_fk_posthog_t" FOREIGN KEY ("loop_trigger_id") REFERENCES "posthog_task_loop_trigger" ("id") DEFERRABLE INITIALLY DEFERRED;
CREATE UNIQUE INDEX "task_loop_fire_trigger_key_unique" ON "posthog_task_loop_fire" ("loop_trigger_id", "fire_key") WHERE "loop_trigger_id" IS NOT NULL;
CREATE UNIQUE INDEX "task_loop_fire_loop_key_unique" ON "posthog_task_loop_fire" ("loop_id", "fire_key") WHERE "loop_trigger_id" IS NULL;
CREATE INDEX "posthog_task_loop_fire_loop_id_50f03f32" ON "posthog_task_loop_fire" ("loop_id");
CREATE INDEX "posthog_task_loop_fire_team_id_9197966c" ON "posthog_task_loop_fire" ("team_id");
CREATE INDEX "posthog_task_loop_fire_loop_trigger_id_09f029d3" ON "posthog_task_loop_fire" ("loop_trigger_id");
CREATE INDEX "task_loop_fire_loop_ct_idx" ON "posthog_task_loop_fire" ("loop_id", "created_at");
COMMIT;

products/tasks/backend/migrations/0064_task_loop_index.py

--
-- Concurrently create index posthog_task_loop_idx on field(s) loop of model task
--
SET lock_timeout = 0;
SET statement_timeout = 0;
CREATE INDEX CONCURRENTLY "posthog_task_loop_idx" ON "posthog_task" ("loop_id");

products/tasks/backend/migrations/0065_looptrigger_completed_at.py

BEGIN;
--
-- Add field completed_at to looptrigger
--
ALTER TABLE "posthog_task_loop_trigger" ADD COLUMN "completed_at" timestamp with time zone NULL;
COMMIT;

products/tasks/backend/migrations/0066_loopfire_trigger_set_null.py

BEGIN;
--
-- Alter field loop_trigger on loopfire
--
-- (no-op)
COMMIT;

products/tasks/backend/migrations/0067_loop_creator.py

BEGIN;
--
-- Add field creator to loop
--
ALTER TABLE "posthog_task_loop" ADD COLUMN "creator_id" integer NULL;
CREATE INDEX "posthog_task_loop_creator_id_4c8732da" ON "posthog_task_loop" ("creator_id");
COMMIT;

products/tasks/backend/migrations/0068_loop_creator_backfill.py

BEGIN;
--
-- Raw Python operation
--
-- THIS OPERATION CANNOT BE WRITTEN AS SQL
COMMIT;

Last updated: 2026-07-21 22:59 UTC (71ec282)

Django migration risk — migration analysis complete

We've analyzed your migrations for potential risks.

Summary: 3 Safe | 3 Needs Review | 0 Blocked

⚠️ Needs Review

May have performance impact

tasks.0063_loop_looptrigger_loopfire
  └─ #1 ✅ CreateModel
     Creating new table is safe
     model: Loop
  └─ #2 ✅ CreateModel
     Creating new table is safe
     model: LoopTrigger
  └─ #3 ✅ CreateModel
     Creating new table is safe
     model: LoopFire
  └─ #4 ⚠️ AlterField
     Field alteration may cause table locks or data loss (check if changing type or constraints)
     model: task, field: origin_product, field_type: CharField
  └─ #5 ✅ AddField
     Adding nullable field requires brief lock
     model: task, field: loop
  │
  └──> ℹ️  INFO:
       ℹ️  Skipped operations on newly created tables (empty tables
       don't cause lock contention).
tasks.0066_loopfire_trigger_set_null
  └─ #1 ⚠️ AlterField
     Field alteration may cause table locks or data loss (check if changing type or constraints)
     model: loopfire, field: loop_trigger, field_type: ForeignKey
tasks.0068_loop_creator_backfill
  └─ #1 ⚠️ RunPython: RunPython data migration needs review for performance

✅ Safe

Brief or no lock, backwards compatible

tasks.0064_task_loop_index
  └─ #1 ✅ SafeAddIndexConcurrently
     PostHog concurrent-index helper: idempotent (timeout disabling + invalid-leftover recovery)
     model: task, index: posthog_task_loop_idx
tasks.0065_looptrigger_completed_at
  └─ #1 ✅ AddField
     Adding nullable field requires brief lock
     model: looptrigger, field: completed_at
tasks.0067_loop_creator
  └─ #1 ✅ AddField
     Adding nullable field requires brief lock
     model: loop, field: creator

📚 How to Deploy These Changes Safely

AddField:

This operation acquires a brief lock but doesn't rewrite the table.

Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up.

RunPython:

Use batching for large data migrations:

  • Use .iterator() to avoid loading all rows into memory
  • Use .bulk_update() instead of saving individual objects
  • Batch size: 1,000-10,000 rows per batch
  • Add pauses between batches
  • Consider background jobs for very large updates (millions of rows)

See the migration safety guide

Last updated: 2026-07-21 22:59 UTC (71ec282)

@trunk-io

trunk-io Bot commented Jul 14, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@charlesvien

Copy link
Copy Markdown
Member Author

@greptileai

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Security Review

The external loop trigger endpoint can bypass its advertised payload-size cap when Content-Length is missing or inaccurate.

Reviews (1): Last reviewed commit: "test(mcp): update unit test snapshots" | Re-trigger Greptile

Comment thread products/tasks/backend/presentation/views/loops.py Outdated
@charlesvien
charlesvien force-pushed the feat/loops branch 6 times, most recently from 0dbba35 to 63e38ab Compare July 18, 2026 00:17
@charlesvien
charlesvien force-pushed the feat/loops branch 2 times, most recently from 67b2c2c to 5671efe Compare July 21, 2026 13:41
@charlesvien
charlesvien marked this pull request as ready for review July 21, 2026 14:11
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested review from a team July 21, 2026 14:12
@pr-assigner-resolver-posthog

Copy link
Copy Markdown

👀 Auto-assigned reviewers

These soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:

  • @PostHog/team-devex (owners.yaml)

Soft owners come from each directory's owners.yaml and each product's product.yaml (resolved nearest-file-wins). The locator after each owner is the file that decided it. Generated files and lockfiles are ignored when deciding ownership.

Comment thread products/tasks/backend/presentation/views/loops.py
Comment thread products/tasks/backend/logic/services/loop_runs.py
Comment thread products/tasks/backend/presentation/views/loops.py Outdated
Comment thread products/tasks/mcp/tools.yaml
@veria-ai

veria-ai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 18 · PR risk: 0/10

Comment thread products/tasks/backend/facade/loops.py
Comment thread products/tasks/backend/logic/services/loop_runs.py
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit 71ec282 · box box-26dcbdb63e4a · ready in 857s (push → usable) · build log · rebuilds on every push, torn down on close

Comment thread posthog/api/advanced_activity_logs/viewset.py
Comment thread products/tasks/backend/facade/loops.py
@github-actions
github-actions Bot requested a deployment to preview-pr-70604 July 21, 2026 17:25 In progress
@github-actions
github-actions Bot requested a deployment to preview-pr-70604 July 21, 2026 17:33 In progress
@github-actions
github-actions Bot requested a deployment to preview-pr-70604 July 21, 2026 17:42 In progress
@github-actions
github-actions Bot requested a deployment to preview-pr-70604 July 21, 2026 17:55 In progress
@github-actions
github-actions Bot requested a deployment to preview-pr-70604 July 21, 2026 22:50 In progress
@posthog

posthog Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

@charlesvien
charlesvien enabled auto-merge (squash) July 21, 2026 23:22
@charlesvien
charlesvien merged commit c0db7aa into master Jul 21, 2026
300 of 304 checks passed
@charlesvien
charlesvien deleted the feat/loops branch July 21, 2026 23:42
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 22, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-22 00:06 UTC Run
prod-us ✅ Deployed 2026-07-22 00:20 UTC Run
prod-eu ✅ Deployed 2026-07-22 00:20 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants