Running a personal-feel cold outreach cadence on Attio (founder emailing creators, composer sends through connected Gmail). Verified live 2026-08-23 on a Pro workspace.
1. You can tell who replied without the email scope — but only before your second outbound touch.
An API token scoped to objects/lists/entries but NOT email gets 403 on GET /v2/emails, so message bodies are unreadable. Reply state is still derivable: POST /v2/objects/people/records/query returns first_email_interaction and last_email_interaction on every person, each a list of entries with interacted_at (take the one whose active_until is null).
first == last-> exactly one interaction, i.e. you emailed them and nothing has happened since.now - last >= 3 days AND first == lastis a complete "due for a follow-up" predicate from one paginated query.
The correction that cost me a wrong design: first != last does NOT mean "they replied." These attributes count interactions, not directions. I sent a second outbound email to a pilot record (one send on 08-13, another on 08-23) and watched last_email_interaction move to the new send while first stayed on 08-13, with zero inbound mail. A follow-up you send yourself splits first from last exactly the way a reply does. So the test is only valid for records that have received exactly one outbound email; after the follow-up, discriminate with a stage value plus a stored ping timestamp (a last newer than your recorded ping is a reply), or read direction off the record's Emails tab.
This matters because Attio's UI has no Sent view (/emails/ shows only Drafts, Outbox, Templates, Failed, all reading 0 after a batch goes out), so these attributes are the only bulk reconciliation surface for what was actually sent.
Caveat: mailbox sync writes these for the whole connected mailbox, so people auto-created from Gmail sync carry them too. Filter to your own records (e.g. a custom channel_url being set) before counting. Sync lag is about a minute; the attribute is not updated at send time.
2. Workflows can detect the cadence but cannot send it.
Attio workflows DO have a Recurring schedule trigger, so "every morning, find everyone Contacted more than 3 days ago" is expressible. There is no native send-email block: the only email action is Enroll in sequence, and sequences force an unsubscribe footer on every message including single-step ones (https://attio.com/help/reference/automations/sequences/email-deliverability-guide). Third-party blocks (Outreach, Mailchimp, Mixmax) send from those systems, not your connected Gmail. So the automatable half of a follow-up cadence is detection and bookkeeping; the send stays manual or goes through Gmail with mailbox sync attaching it.
3. Attio never blocks an empty template variable.
An unset attribute renders as an empty string and the email sends anyway. A template containing variables replaces the compose footer's send button with Review, which opens a "Previewing" screen with every chip resolved — that preview is the ONLY place a missing value is visible. My pilot preview read " - loved your video on whether or not to get into goats." with a leading space where the name belonged, and nothing warned. Enforce "a missing value blocks the send" upstream in your own tooling; Attio will not do it.
Stage design that falls out of this. One stage per touch (Contacted, then Followed Up) and one attribute-changed workflow per hop: trigger on Last email interaction changed, Find list entries filtered on the previous stage AND Parent Record is {Updated Person}, then update the entry to the next stage. Consecutive hops are disjoint because each guards on a different current stage, and the stage becomes the double-ping guard. The parent-record filter is not optional at scale: without it the block returns the first 100 of your Lead entries and the right one usually is not among them. Confirmed working end to end when the pilot send flipped Lead -> Contacted about a minute after the send, via the sync-written attribute.