Part of #181. Spec: #186. Slice 5 of 11 from #185.
Question
How do we ship this slice as one draft PR on PostHog/posthog behind workflows-audience?
Delivers. The Recipients tab becomes Audience's default: one row per recipient from the listing API, address search, and the "N persons can't be reached" line that opens the persons list already filtered to persons with no email property.
Spec. #186, approved by Michael on 2026-10-01.
Blocked by. #187, #190
Acceptance criteria
/audience opens Recipients. Columns: Recipient (with a Suppressed tag), All marketing, Topics (explicit subscribed/unsubscribed topics as tags, "No preference on any topic" otherwise), Persons (name, count or "No person"), Last sent (header says it covers 30 days).
- Cursor paging, 50 rows a page. The search box sends
search to the API, debounced.
- Above the table, one muted line: "N persons can't be reached because they have no email property". It is hidden at 0. The link opens
urls.persons() with a #q= DataTableNode over an ActorsQuery filtered by { type: 'person', key: 'email', operator: 'is_not_set' }.
- Loading, empty ("No recipients yet"), no-match and error states are different screens. The error state asks to narrow the search.
- A row opens
/audience/recipients/<email>.
- The frontend uses the generated
messaging_recipients types. Stories at full width and ~520px, with screenshots.
- A draft PR on
PostHog/posthog with proof (tests plus screenshots for visual work), an adversarial review, and green CI. Never ready, never queued.
Tracking events
audience unreachable persons opened (count).
Test seam
Jest test through the real recipients logic and the real unreachablePersonsUrl() with the API mocked at the HTTP layer: typing a search issues a request with search, the next page sends the returned cursor, and the unreachable link decodes to an ActorsQuery with the email is_not_set filter. The persons scene itself is not mocked away from the URL contract: decode the q hash the way personsSceneLogic does.
Part of #181. Spec: #186. Slice 5 of 11 from #185.
Question
How do we ship this slice as one draft PR on
PostHog/posthogbehindworkflows-audience?Delivers. The Recipients tab becomes Audience's default: one row per recipient from the listing API, address search, and the "N persons can't be reached" line that opens the persons list already filtered to persons with no
emailproperty.Spec. #186, approved by Michael on 2026-10-01.
Blocked by. #187, #190
Acceptance criteria
/audienceopens Recipients. Columns: Recipient (with a Suppressed tag), All marketing, Topics (explicit subscribed/unsubscribed topics as tags, "No preference on any topic" otherwise), Persons (name, count or "No person"), Last sent (header says it covers 30 days).searchto the API, debounced.urls.persons()with a#q=DataTableNodeover anActorsQueryfiltered by{ type: 'person', key: 'email', operator: 'is_not_set' }./audience/recipients/<email>.messaging_recipientstypes. Stories at full width and ~520px, with screenshots.PostHog/posthogwith proof (tests plus screenshots for visual work), an adversarial review, and green CI. Never ready, never queued.Tracking events
audience unreachable persons opened(count).Test seam
Jest test through the real recipients logic and the real
unreachablePersonsUrl()with the API mocked at the HTTP layer: typing a search issues a request withsearch, the next page sends the returned cursor, and the unreachable link decodes to anActorsQuerywith theemail is_not_setfilter. The persons scene itself is not mocked away from the URL contract: decode theqhash the waypersonsSceneLogicdoes.