Skip to main content
All Docs
FeaturesBlockManOSUpdated April 9, 2026

Contact Linking Across Roles

Contact Linking Across Roles

BlockManOS can detect when the same individual appears in multiple roles — owner, contractor, OMC director, or agentOS contact — by matching records on shared email address or phone number. Records remain separate per role but a unified view is surfaced wherever a match is found.

How It Works

Matching is performed at query time when a record page is loaded. No background sync job runs and no denormalised link table is maintained.

Match Criteria

FieldMethod
EmailCase-insensitive exact match
PhoneLast 7 digits comparison (normalises formatting differences)

A match can be made on email only, phone only, or both. The panel indicates which field(s) produced the match.

Roles Searched

  • Owners — matched against all other owner records in the organisation.
  • Contractors — matched against contractor records.
  • OMC Directors — matched against director records.
  • agentOS Contacts — matched via the agentos_sync_mappings table (requires agentOS integration to be active).

LinkedRolesPanel

The LinkedRolesPanel component renders on any detail page where contact linking is enabled. It is currently integrated into the Owner Detail page, appearing between the summary statistics and the contact details section.

What the Panel Shows

  • A list of matched records with name, role badge, and contextual description.
  • Match indicator — whether the link was made by email, phone, or both.
  • The matched email address and/or phone number.
  • A notes preview (if notes exist on the linked record).
  • A direct link to the matched record's own detail page.

The panel does not render if no links are found, keeping the page uncluttered for records with no cross-role presence.

Merged Activity Notes

Click Show Merged Notes in the panel header to expand a combined activity view. This collects:

  • Notes from the source record.
  • Notes from all linked records.
  • Notes from associated maintenance requests across all linked roles.

This provides a single activity timeline for agents managing a contact who holds more than one role.

Dismissing False Positives

If two records share an email or phone number but are not the same person, a user can dismiss the link:

  1. Click the dismiss (eye-off) icon on the linked record row.
  2. The link is immediately hidden and a dismissal reason is stored.
  3. Future queries for the same source record will exclude the dismissed pair.

Dismissals require admin permissions and are written to the audit log.

Restoring a Dismissed Link

Dismissed links can be undone via the contactLink.restoreLink tRPC procedure. Use contactLink.listDismissed to retrieve the full set of dismissed matches for a given record.

tRPC API Reference

All procedures are on the contactLink router.

contactLink.getLinkedRoles

Returns all active (non-dismissed) linked roles for a source record.

Access: orgProcedure (any authenticated org member)

Input:

{
  sourceType: "owner" | "contractor" | "director";
  sourceId: string;
}

Output:

{
  linkedRoles: Array<{
    roleType: "owner" | "contractor" | "director" | "agentos_contact";
    recordId: string;
    name: string;
    context: string;
    matchedOn: "email" | "phone" | "both";
    email?: string;
    phone?: string;
    notes?: string;
  }>;
}

contactLink.getMergedNotes

Returns a merged list of notes from the source record and all linked roles, including associated maintenance request notes.

Access: orgProcedure

Input:

{
  sourceType: "owner" | "contractor" | "director";
  sourceId: string;
}

Output:

{
  notes: Array<{
    roleType: "owner" | "contractor" | "director" | "agentos_contact";
    recordId: string;
    content: string;
    // additional metadata fields
  }>;
}

contactLink.dismissLink

Dismisses a linked pair as a false positive. Requires admin access. Audit logged.

Access: adminProcedure

Input:

{
  sourceType: "owner" | "contractor" | "director";
  sourceId: string;
  targetType: "owner" | "contractor" | "director" | "agentos_contact";
  targetId: string;
  reason: string;
}

contactLink.restoreLink

Restores a previously dismissed link. Requires admin access. Audit logged.

Access: adminProcedure

Input:

{
  sourceType: "owner" | "contractor" | "director";
  sourceId: string;
  targetType: "owner" | "contractor" | "director" | "agentos_contact";
  targetId: string;
}

contactLink.listDismissed

Lists all dismissed links for a given source record.

Access: orgProcedure

Input:

{
  sourceType: "owner" | "contractor" | "director";
  sourceId: string;
}

Database Schema

contact_role_type Enum

CREATE TYPE contact_role_type AS ENUM (
  'owner',
  'contractor',
  'director',
  'agentos_contact'
);

contact_link_dismissals Table

Stores user-dismissed false-positive matches to permanently exclude them from future getLinkedRoles queries.

ColumnTypeDescription
source_typecontact_role_typeRole type of the originating record
source_idtextID of the originating record
target_typecontact_role_typeRole type of the dismissed match
target_idtextID of the dismissed record
reasontextUser-supplied reason for dismissal
dismissed_bytextUser ID who performed the dismissal
dismissed_attimestampWhen the dismissal was recorded

Audit Logging

The following operations are written to the platform audit log:

  • dismissLink — records who dismissed which pair and the stated reason.
  • restoreLink — records who restored the link.

Read operations (getLinkedRoles, getMergedNotes, listDismissed) are not audit-logged.