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
| Field | Method |
|---|---|
| Case-insensitive exact match | |
| Phone | Last 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_mappingstable (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:
- Click the dismiss (eye-off) icon on the linked record row.
- The link is immediately hidden and a dismissal reason is stored.
- 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.
| Column | Type | Description |
|---|---|---|
| source_type | contact_role_type | Role type of the originating record |
| source_id | text | ID of the originating record |
| target_type | contact_role_type | Role type of the dismissed match |
| target_id | text | ID of the dismissed record |
| reason | text | User-supplied reason for dismissal |
| dismissed_by | text | User ID who performed the dismissal |
| dismissed_at | timestamp | When 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.