CitationBenchTalk to Sales
Concepts

Link Building: a CRM data model for partners, contacts, and events

CitationBench models link building as a CRM — shared accounts, per-workspace relationships, typed events — so every outreach interaction carries years of context.

CitationBench treats link building as a CRM problem, not a one-shot blast problem. Every partner you encounter — through SERP outreach, competitor backlink discovery, or inbound emails — becomes a tracked account, with contacts, relationships, and events that persist across years. The data model exists so every interaction has context: who you talked to last time, what was agreed, how much they charge, who else in the agency has worked with them.

The short version

  • Account = the partner domain (e.g. engineering-blog.com). Shared system-wide — every agency benefits from cross-portfolio knowledge of who's responsive, who charges, who refuses.
  • Contact = a person at that domain. Tied to one account.
  • Relationship = your workspace's relationship with that account. Pipeline-staged (PROSPECTING → CONTACTED → ENGAGED → LINK_PLACED). One per workspace.
  • Event = a single interaction: email sent, email received, link placed, status changed, note added.

Four endpoints families touch this model:

Why we modeled it this way

Three design constraints shaped this:

1. Accounts must be shared across agencies. If someblog.com has DO_NOT_CONTACT status (they explicitly asked to be removed), every agency on the platform benefits from that knowledge. We can't let one agency's outreach mess up another's relationship. So accounts are system-wide and shared — but each agency's view of the relationship is private.

2. Relationships are per-workspace, not per-agency. Two agencies might pitch the same blog about different topics. The relationship state (PROSPECTING vs ENGAGED vs LINK_PLACED) is meaningful only inside one client–partner pairing. Two parallel relationships for the same partner is normal.

3. Events are the ground truth. Statuses can lie. Events can't. Every action — including auto-respond drafts, auto-sent emails, rate negotiations, link placements — is logged as a typed event. The relationship's status is derived; the event log is the source of truth.

The data model

LinkBuildingAccount (the partner — shared system-wide)

{
  "id": "acct_***",
  "domain": "engineering-blog.com",
  "name": "Engineering Blog Weekly",
  "source": "SEARCH",
  "sourceDetails": { "campaignId": "scmp_***", "serpRank": 4 },
  "rawNotes": "Editor responsive within 24h; sent us specs in past",
  "bestContactId": "ct_***",
  "isActive": true,
  "lastContactedAt": "2026-05-12T...",
  "systemContactStatus": "RATE_ESTABLISHED",
  "firstContactedAt": "2025-11-04T...",
  "firstContactedByOrgId": "ws_some-other-agency-client",
  "lastContactedByOrgId": "ws_acme",
  "coolingOffUntil": null,
  "contactAttemptCount": 8,
  "priceLinkInsertion": 350,
  "priceGuestPost": 900,
  "pricesUpdatedAt": "2026-04-10T...",
  "domainRating": 62,
  "organicTraffic": 48500,
  "organicKeywords": 11200,
  "referringDomains": 1840,
  "backlinks": 18400,
  "ahrefStatsUpdatedAt": "2026-05-15T..."
}

Key fields:

  • systemContactStatus is the shared status across all agencies. DO_NOT_CONTACT here means no one can email them.
  • priceLinkInsertion / priceGuestPost get filled in as you negotiate. Once one agency learns the rates, every agency benefits.
  • domainRating and traffic stats refresh from Ahrefs periodically.

LinkBuildingContact (a person at an account)

{
  "id": "ct_***",
  "accountId": "acct_***",
  "firstName": "Marina",
  "lastName": "Olafsson",
  "email": "marina@engineering-blog.com",
  "position": "Senior Editor",
  "seniority": "manager",
  "linkedinUrl": "https://linkedin.com/in/marinaolafsson",
  "twitterHandle": "@marolafsson",
  "notes": "Prefers email; doesn't read DMs",
  "apolloPersonId": "5f3c1b18-7d8b-4f3e-9c4b-2c1a3e0a9b8f"
}

LinkBuildingRelationship (per-workspace pipeline)

{
  "id": "rel_***",
  "organizationId": "ws_acme",
  "accountId": "acct_***",
  "source": "SEARCH",
  "sourceDetails": { "campaignId": "scmp_***", "serpRank": 4 },
  "rawNotes": "Pitched our capacity-tracking article; they expressed interest",
  "targetBlogPost": "https://engineering-blog.com/best-pm-tools",
  "ourContent": "https://acme.com/blog/engineering-team-capacity-tracking",
  "status": "ACTIVELY_ENGAGED",
  "priority": "HIGH",
  "contactedByUs": true,
  "firstContactedByUsAt": "2026-05-08T...",
  "lastContactedByUsAt": "2026-05-22T...",
  "contactedByOtherOrg": false,
  "firstContactedAt": "2026-05-08T..."
}

Pipeline statuses

PROSPECTING          → identified but not contacted
APPROVED_TO_CONTACT  → reviewer / approver said go
QUEUED               → in the outreach queue
CONTACTED_BY_US      → first email sent
CONTACTED_BY_OTHER_ORG → another agency's relationship is active (heads-up for us)
ACTIVELY_ENGAGED     → multi-message thread
LINK_PLACED          → link is live
NOT_INTERESTED       → they passed
INACTIVE             → cool-off period

LinkBuildingEvent (the log)

{
  "id": "evt_***",
  "relationshipId": "rel_***",
  "contactId": "ct_***",
  "type": "EMAIL_SENT",
  "subject": "Section 7 of your PM tools roundup",
  "content": "Hi Marina, ...",
  "metadata": { "instantlyMessageId": "im_***", "campaignId": "scmp_***" },
  "occurredAt": "2026-05-08T09:14:23Z"
}

Event types: EMAIL_SENT, EMAIL_RECEIVED, LINK_PLACED, REJECTED, CALL_SCHEDULED, NOTE_ADDED, STATUS_CHANGED, CONTACT_RESEARCH.

How it interacts with other concepts

ConceptRelationship
Agentlink_hunter skill orchestrates SERP → contact discovery → outreach → response handling
WorkspacesRelationships and events are workspace-scoped; accounts and contacts are global
Approval WorkflowsOutreach send is approval-gated by default; inbound auto-respond can be gated by eval gates
Eval GatesThe rule layer that decides when inbound negotiation escalates to a human
SERP outreachCreates new relationships from research.serp results
Competitor outreachCreates new relationships from research.competitor.backlinks results

Common patterns

1. The SERP-driven flow

link_building.serp_outreach    (input: keyword)
  ↓ runs research.serp
  ↓ filters by DR, prior contact, account status
  ↓ runs link_building.crm.contact.discover (Apollo) for each surviving domain
  ↓ drafts personalized outreach for each contact
  ↓ pauses at WAITING_APPROVAL

human approves
  ↓ link_building.campaign.send_email per draft
  ↓ creates EMAIL_SENT events
  ↓ relationship status: CONTACTED_BY_US

2. The competitor-driven flow

Same shape, but the seed is a competitor domain — research.competitor.backlinks returns sites that link to the competitor; we pitch the same sites with our content.

3. The inbound flow

inbound email lands → InboundMessage
  ↓ classified (intent, sender DR, prior history)
  ↓ eval gates checked
  ↓ if all gates pass: agent drafts and sends a response autonomously
  ↓ if any gate trips: WAITING_APPROVAL with the gate name + reason

4. CRM read-only patterns

# All ACTIVELY_ENGAGED partners for this workspace
curl -G .../v1/link-building/crm/relationship \
  --data-urlencode "status=ACTIVELY_ENGAGED"

# All events with one partner across all our workspaces
curl -G .../v1/link-building/crm/event \
  --data-urlencode "accountId=acct_***"

5. Pricing intelligence

Because accounts are shared system-wide, when one agency negotiates a rate (e.g., priceLinkInsertion: $350), every subsequent agency sees that price up front. No one has to re-negotiate from zero.

On this page