You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Email API: API sends origin and the one-flow-per-team index #468
Can the hog flow table hold exactly one API sends workflow per team?
Delivers end to end. A new origin_product choice api_sends ("API sends") and a partial unique index on team_id where origin_product = 'api_sends', built concurrently. A second API sends flow for a team fails at the database. Spec sections: "The hidden API sends workflow", "Schema and data".
This is the migration-only PR the repo rules ask for. No behavior uses the choice yet.
Test seams. One model test: creating a second api_sends flow for the same team raises an integrity error; another team and other origins are unaffected.
Write scope.products/workflows/backend/facade/enums.py (HogFlowOriginProduct), products/workflows/backend/models/hog_flow/hog_flow.py (constraint), a new migration under products/workflows/backend/migrations/, the model test.
Proof.
Red: the duplicate-flow test passes a second insert before the index exists.
Green: the same test raises after the migration.
hogli migration checks and /django-migrations pass: the index builds concurrently and the migration is safe to run on a live table.
Acceptance criteria
HogFlowOriginProduct.API_SENDS exists with the label "API sends".
Two api_sends flows for one team cannot both exist; two for different teams can.
Flows with other origins are not constrained.
The index migration builds concurrently and lands alone in its PR.
/tdd red-green-refactor, outside-in, at the seams named above. Real Postgres, Valkey and ClickHouse; mock only what the seam section allows.
Repo skills apply where the diff touches their area: /writing-tests, /improving-drf-endpoints, /django-migrations, /adding-api-scopes, /writing-ui-components, /writing-kea-logics, /writing-user-facing-copy, /writing-code-comments.
Public repo: no usage numbers, customer names, internal metrics, Slack content or security reasoning in code, commits or the PR. Fixture addresses use example.com. The private note the map points at is background only; never quote it.
Shipped means a draft PR handed off for review (see the map's Notes for remotes and branches). Never mark ready, enqueue or merge.
Runtime: Claude Code background Agent
Worker: implement-468-api-sends-index
Cast: Opus 5.5, high. Wall-time cap: 120 minutes.
Ships alone as a migration-only draft PR from spec/email-send-api-migration; the integration branch spec/email-send-api branches from its final tip.
Branch:Silthus:spec/email-send-api-migration, from upstream/master at 6d41818cfa6.
Reviewed head:ce0843adff0855b68947d192c6e242666138f3ee. The integration branch spec/email-send-api can branch from this tip.
What shipped
HogFlowOriginProduct.API_SENDS ("API sends").
Migration workflows.0033_hogflow_unique_api_sends_flow_per_team: CREATE UNIQUE INDEX CONCURRENTLY "unique_api_sends_flow_per_team" ON posthog_hogflow (team_id) WHERE origin_product = 'api_sends', through CreateIndexConcurrently in SeparateDatabaseAndState, with the matching UniqueConstraint in model state.
The hog flow create serializer refuses origin_product: "api_sends" with a 400 (review finding, see below).
Regenerated OpenAPI types for the workflows frontend and the MCP service.
Proof
Red:test_a_team_holds_one_api_sends_flow failed with IntegrityError not raised before the migration. The other-team and other-origin cases passed.
Green: after the migration, both touched test files passed in full on a test database built by running every migration.
Create guard:test_api_sends_origin_is_refused_on_create answered 201 before the guard and 400 after it.
Migration checks:analyze_migration_risk --fail-on-blocked rates 0033 safe. makemigrations --check reports no drift. sqlmigrate shows the concurrent unique index with lock and statement timeouts disabled.
Gate: the pre-push hogli ci:preflight --strict passed on both pushes.
Review (qa-swarm, two rounds)
Round 1, GPT-6.1 Sol (Correctness, Risk, Design) and Opus 5.5 (Fresh eyes): all four lanes found that the public create endpoint and the MCP create tool accepted api_sends. A second create for a team returned a 500 from the index, and the first let a caller pick the graph of the flow PostHog owns. Fixed with validate_origin_product and an endpoint test. Alternative rejected: dropping the value from the create serializer's choice set, because a subset choice set needs an explicit OpenAPI enum name override.
Round 1 nits: ambiguous "the API" wording in the enum docstring and model comment, and a redundant parameterized test case. Fixed.
Round 2 on the fix diff, both model families: no findings.
CodeRabbit skipped: retired on this repository.
Gaps
CI on the fork PR waits for a maintainer to approve the workflow runs. No check has run yet.
The PR assignee could not be set from the fork (missing permission).
Once the API sends flow can open in the builder, a builder test run that posts the whole flow as configuration with origin_product: "api_sends" hits the new create guard. Email API: Django creates and owns the hidden API sends workflow #469 should keep origin_product out of that payload or exempt the path.
get_or_create_api_sends_flow creates the flow through the ORM, so the serializer guard does not block it.
Activity
Silthus commentedon Oct 14, 2026
Runtime: Claude Code background Agent
Worker: implement-468-api-sends-index
Cast: Opus 5.5, high. Wall-time cap: 120 minutes.
Ships alone as a migration-only draft PR from
spec/email-send-api-migration; the integration branchspec/email-send-apibranches from its final tip.Silthus commentedon Oct 14, 2026
Claimed by conductor dispatch. Runtime: Claude Code background Agent, worker implement-468-api-sends-index, Opus 5.5 high.
Silthus commentedon Oct 14, 2026
Resolved by draft PR PostHog/posthog#116915.
Silthus:spec/email-send-api-migration, fromupstream/masterat6d41818cfa6.ce0843adff0855b68947d192c6e242666138f3ee. The integration branchspec/email-send-apican branch from this tip.What shipped
HogFlowOriginProduct.API_SENDS("API sends").workflows.0033_hogflow_unique_api_sends_flow_per_team:CREATE UNIQUE INDEX CONCURRENTLY "unique_api_sends_flow_per_team" ON posthog_hogflow (team_id) WHERE origin_product = 'api_sends', throughCreateIndexConcurrentlyinSeparateDatabaseAndState, with the matchingUniqueConstraintin model state.origin_product: "api_sends"with a 400 (review finding, see below).Proof
test_a_team_holds_one_api_sends_flowfailed withIntegrityError not raisedbefore the migration. The other-team and other-origin cases passed.test_api_sends_origin_is_refused_on_createanswered 201 before the guard and 400 after it.analyze_migration_risk --fail-on-blockedrates 0033 safe.makemigrations --checkreports no drift.sqlmigrateshows the concurrent unique index with lock and statement timeouts disabled.hogli ci:preflight --strictpassed on both pushes.Review (qa-swarm, two rounds)
api_sends. A second create for a team returned a 500 from the index, and the first let a caller pick the graph of the flow PostHog owns. Fixed withvalidate_origin_productand an endpoint test. Alternative rejected: dropping the value from the create serializer's choice set, because a subset choice set needs an explicit OpenAPI enum name override.Gaps
Follow-ups for #469
configurationwithorigin_product: "api_sends"hits the new create guard. Email API: Django creates and owns the hidden API sends workflow #469 should keeporigin_productout of that payload or exempt the path.get_or_create_api_sends_flowcreates the flow through the ORM, so the serializer guard does not block it.