Skip to content

Email API: API sends origin and the one-flow-per-team index #468

Closed

Description

@Silthus

Map: Map: send email from code with one PostHog call
Spec: Spec: email send API

Cast: Opus 5.5, high
Repository: PostHog/posthog

Question

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.

How to build (map #461)

  • /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.

Activity

  1. self-assigned this
    on Oct 14, 2026
  2. Silthus commented on Oct 14, 2026

    @Silthus
    OwnerAuthor

    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.

  3. Silthus commented on Oct 14, 2026

    @Silthus
    OwnerAuthor

    Claimed by conductor dispatch. Runtime: Claude Code background Agent, worker implement-468-api-sends-index, Opus 5.5 high.

  4. Silthus commented on Oct 14, 2026

    @Silthus
    OwnerAuthor

    Resolved by draft PR PostHog/posthog#116915.

    • 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).

    Follow-ups for #469

    • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions