feat(desktop): Implement loops ➿ - #70604
Conversation
🤖 CI report
|
| 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 |
⚠️ 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
⚠️ 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)
|
0dbba35 to
63e38ab
Compare
67b2c2c to
5671efe
Compare
👀 Auto-assigned reviewersThese soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:
Soft owners come from each directory's |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 18 · PR risk: 0/10 |
🦔 Hogbox preview · ✅ ready▶ Open the preview
commit |
|
👋 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. |
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/.Loop,LoopTriggerandLoopFire(per-fire dedup) models, plus a nullableloopFK onTask. Triggers: cron or one-time schedules, GitHub webhook events (pull_request,issues,issue_comment,push) and an external API triggerfire_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 pipelineloops/: CRUD plusrun/(manual fire),trigger/(external fire, callable with a project secret API key via the newloop:writescope, the first write-capable PSAK scope),runs/history and a dry-runpreview/posthog/urls.pybecomes 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 alongsidecontext.mdand/or rewrite a canvas dashboard; context-attached loops must be team-visibledisabled_reason), retention sweeps (200 tasks kept per loop, fire records pruned after 7 days)loops-*MCP tools plus aloops-reviewcard (MCP UI app): an agent assembles a loop from natural language and the user confirms with a Create button instead of it being created blindTemporal changes
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 flagsrun-loopworkflow (single activity, 2 min timeout, 3 attempts) translates each schedule occurrence into afire_loop()call. The agent run itself still starts the existingprocess-taskworkflow on the same task queue: loops are a firing layer, not a new execution enginecancel_previousalso signals the superseded run's workflow (complete_taskwith cancelled), so the sandbox actually winds down instead of only the DB row flippingupdate_task_run_statusnow 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 defaultsCloud sandbox changes
completedon their natural end (agent finishes or idles out) instead of stayingin_progressforever. Interactive runs keep the always-followable behaviorloop:writeis 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 exactlyfile_system:read+file_system:writeadded, never the broadfullsurfacecreate_prsbehavior and defaults off (report-only);watch_ci/fix_review_commentsopt the run into the existing CI follow-up roundsHow did you test this code?
test_loops_api,test_loop_service,test_loop_runs,test_loop_github_events,test_loop_notifications,test_loop_retention,test_loop_lifecycleandtest_loop_reconciliation👉 Stay up-to-date with PostHog coding conventions for a smoother review.
Automatic notifications
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 thetasksandloopsfeature flags.