Skip to content

PR CRM — Attio Workflows

Attio is the team’s day-to-day surface for contact + campaign tracking. Supabase is the master database underneath; Attio is a polished mirror you actually click around in. Every change you make in Attio syncs back to Supabase via webhook, and every send/open/reply/bounce reported by Smartlead flows into Attio in real time.

This guide covers how to use Attio to its full potential for per-campaign tracking, daily triage, and relationship management.


Three objects in Attio matter for PR work:

ObjectWhat it isWhat lives there
PeopleEvery contact in the database (~13K). Mix of journalists, producers, influencers, editors.Name, email(s), outlet, role, segment, beat, market(s), pitch_status, snoozed_until, last_contacted_at, last_replied_at, bounce_count, verified, social handles, notes
CampaignsOne record per pitch you’ve sent (or are sending).Name, status, start_date, segment_filter_summary, smartlead_campaign_id, sent_count, open_count, reply_count, bounce_count, members
MarketsHierarchical geography (countries → states → cities).Name, type, parent market

The two relationships you care about:

  • People ↔ Campaigns (bidirectional): every campaign has a members list of people; every person has an Associated campaigns list. Adding someone to one side automatically reflects on the other.
  • People ↔ Markets (multi-reference): a person can be tagged with multiple markets (Miami, Florida, USA national).

Open Campaigns → click into any campaign (e.g. “Palace: May Editorial Pitch”). Scroll to the Members section. Every person we pitched in this campaign is there. Click any person to drill into their profile.

Attio’s filter UI on the People object lets you compose conditions across any attribute. Useful patterns:

  • By campaign membership: Associated campaigns contains “Palace: May Editorial Pitch”
  • By state: pitch_status = bounced (or replied, snoozed, unsubscribed, do_not_contact)
  • By recency: last_replied_at is greater than “14 days ago”
  • By market: markets contains Florida
  • By segment/beat: segment = Editorial, beat = Food

Combine conditions with AND/OR. Sort by any attribute. Then save the result as a View so you can revisit it without rebuilding.

Click any person in Attio → scroll to the Associated campaigns section. You see every campaign they’ve been in, ordered by date. Click any campaign to drill back into its details. This is your “have we pitched this person recently?” view.

Use the Notes section on the person record to log relationship context (preferences, past wins, contact preferences, do-not-pitch reasons).

In a person’s record, set:

  • pitch_status = snoozed
  • snoozed_until = the date they should be eligible again

The webhook syncs to Supabase. Future campaign filters automatically exclude them. The daily 7am UTC unsnooze cron flips them back to active once the date passes.

This is a global snooze — applies across all future campaigns, not just one. Per-campaign snooze isn’t supported yet (see Where the model is thin).

5. Mark someone as do-not-contact / unsubscribed

Section titled “5. Mark someone as do-not-contact / unsubscribed”

For people who explicitly said “stop emailing me” or who unsubscribed via Smartlead’s footer link:

  • pitch_status = unsubscribed (Smartlead unsubscribes flow here automatically via the webhook)
  • pitch_status = do_not_contact (manually, for explicit opt-outs)

Both states permanently exclude the person from all campaign filters.


Set these up once and pin them. They become your daily triage surface.

View nameFilterSortUse
Campaign: [Pitch Name]Associated campaigns contains ""last_replied_at descOne per active campaign. Shows the roster + replies floating to top
Replies — last 14 dayslast_replied_at > “14 days ago”last_replied_at descMorning triage. Reply to people while it’s still fresh
Replies to [Pitch Name]Associated campaigns contains "" AND last_replied_at > campaign start datelast_replied_at descClosest proxy to “replied to THIS pitch”
Bounces in [Pitch Name]Associated campaigns contains "" AND pitch_status = bouncedPer-campaign deliverability check
Snoozed — coming back soonpitch_status = snoozed AND snoozed_until < “30 days from now”snoozed_until ascAnticipate who’s about to become eligible
Florida — never pitchedmarkets contains Florida AND pitch_status = active AND last_contacted_at is emptyFind fresh prospects when designing a new campaign. Replace “Florida” with any market
VIPsbeat = VIPlast_replied_at descConcierge view of priority contacts
Pipeline (kanban)(no filter)Group by pitch_statusVisualize 13K contacts as a kanban. Drag-and-drop to update status
Editorial Food — Floridamarkets contains Florida AND segment = Editorial AND beat = FoodPre-saved campaign segments. Replace with any market/segment/beat combo you pitch often
View nameFilterSortUse
Active campaignsstatus = sendingstart_date descWhat’s actively going out right now
Performance leaderboard(none)reply_count descQuick scan of which historical campaigns landed
Palace campaignsname contains “Palace:“start_date descPer-client roll-up. Make a parallel one for each client
Drafts to launchstatus = draftcreated_at descPre-launch staging area

Once you have a saved View, use Attio’s column controls to:

  • Add columns for the attributes you care about most (sent count, last reply date, beat, outlet)
  • Hide columns that aren’t relevant for that view
  • Group records by any attribute (status, segment, market) for kanban-style layouts
  • Pin Views to the sidebar so they’re one click away

Views can be private (just for you) or shared (whole team sees them). Set this when saving — useful for team-wide views like “Replies — last 14 days”.


A reasonable morning rhythm:

  1. Check “Replies — last 14 days” → respond to anyone you haven’t yet
  2. Check “Snoozed — coming back soon” → plan upcoming pitches around who’s about to be eligible
  3. Check active campaigns → eyeball reply/bounce counts on each (run make sync-campaigns first to refresh from Smartlead)
  4. Triage bounces → for any contact with pitch_status = bounced, decide whether to keep them as-is or correct the email and re-tag

Then for whatever pitch you’re working on:

  1. Open “Campaign: [name]” → who’s in it, who replied, who needs a follow-up
  2. Add notes on individual people as you correspond — context lives on the relationship, not in your inbox

last_contacted_at and last_replied_at are global to a person, not per-campaign. So if Sarah was in your March pitch + your May pitch and replied to March, you can’t tell from Attio alone whether she replied to March or May — only that she replied at some point.

For most PR workflows that’s fine — once someone replies, you stop pitching them anyway. But if you want reports like “reply rate of the May Editorial pitch” or “who from the Cherry Blossom DC pitch never opened the email”, you need per-campaign-per-person granularity.

The data exists on the Supabase side already (campaign_contacts join table tracks per-campaign timestamps). Surfacing it in Attio would require a Pitches object (one record per campaign-person pair) — a larger Phase 6 enhancement that’s not built yet. Worth investing in once your team’s reporting needs cross that threshold.


AttributeTypePurpose
name (built-in)personal-nameFirst + last
email_addresses (built-in)email (multi)Primary + alternates. Smartlead matches on this
job_titletextEditor / Producer / etc.
outlettextPublication / network / brand
segmentselectEditorial / TV / Radio / Online / Podcast / Influencer / Photography / Newsletter / Other
beatselectFood / Wine / Travel / Entertainment / Lifestyle / Health / Real Estate / etc. (full list: run make options)
marketsrecord-reference (multi)Geographic tags
pitch_statusselectactive / snoozed / bounced / unsubscribed / do_not_contact
snoozed_untildateWhen a snoozed contact becomes eligible again
last_contacted_attimestampMost recent send (any campaign)
last_replied_attimestampMost recent reply (any campaign)
bounce_countnumberLifetime bounces. Auto-flips status to bounced at 2+
verifiedcheckboxEmail-verification result
verify_statustextVerification message (“ok”, “no MX record”, etc.)
instagram / twitter / linkedin (built-in)textSocial handles
phone_numbers (built-in)phone (multi)Phone numbers
Associated campaignsrecord-reference (multi)Every campaign this person is in
AttributeTypePurpose
nametextDisplay name. Convention: Palace: / Planta: prefix
statusselectdraft / sending / paused / completed
start_datedateWhen the campaign first sent
smartlead_campaign_idtext (unique)The Smartlead-side ID
supabase_idtext (unique)The Supabase-side ID
segment_filter_summarytextHuman-readable: “Florida · Editorial · Food”
sent_count / open_count / reply_count / bounce_countnumberAggregate stats. Refreshed by make sync-campaigns
membersrecord-reference (multi)Every person in this campaign

  • The webhook updates contact-level fields, not campaign-level state. When you click Start on a campaign in Smartlead, the campaign’s status doesn’t auto-update in Attio. Run make sync-campaigns to pull fresh status + stats from Smartlead. Worth running daily or after any UI action.

  • Edits in Attio sync to Supabase within seconds. Changing pitch_status on a person fires the webhook → Supabase row updates → next campaign filter automatically includes/excludes accordingly.

  • Bulk edits via Attio’s UI are fine. Select multiple people in a View → bulk-update an attribute → all changes flow through the webhook.

  • Dedupe before launching big campaigns. If you suspect duplicate Person records (same human under two emails), run make dedupe DRY=1 from the repo to surface them. The dedupe pass moves notes + market links to the surviving record before deleting the duplicate.

  • Snooze ≠ unsubscribe. Snooze is a temporary pause; unsubscribe is permanent. Smartlead’s automated unsubscribe link triggers the latter via webhook.

  • Notes belong on the Person, not the Campaign. Per-campaign annotations would require the Pitches object (not built yet). For now, write campaign-specific context as a note on the relevant Person, mentioning the campaign by name.

  • Don’t manually edit members in Attio for active campaigns. The campaign roster reflects what was sent through Smartlead. Adding/removing someone in Attio doesn’t change the email send queue; it just makes the data inconsistent. Use Smartlead’s UI to pause/edit live campaigns.


  • You’re onboarding someone new to Attio → walk them through “Five core workflows” + “Daily routine”
  • You want to find unpitched contacts in a market → “Saved Views cheatsheet” → “Florida — never pitched” pattern
  • You wonder why a campaign’s status is wrong → “Tips and gotchas” → run make sync-campaigns
  • You hit a wall on per-campaign tracking → “Where the model is thin”

For everything below the Attio surface (creating campaigns, importing CSVs, troubleshooting webhooks, deploying), see RUNBOOK.md.