Core Concepts

Tags

Project88's global tagging system — color-coded tags that work across records, calendar events, conversations, agents, and contacts.

Tags are Project88's universal labeling system. A single tag can be applied to almost anything — a record, a calendar event, a conversation, an agent, a contact — via a polymorphic join table. The same tag picker appears everywhere.

The data model

Two tables (migration 036_tags.sql):

tags

ColumnNotes
idUUID
org_idThe owning org
workspace_idNullable — NULL means org-wide; set means scoped to ws
nameDisplay name (case-insensitively unique within scope)
colorHex color for the pill
display_orderInteger, used by the settings drag-reorder UI
created_byThe user who created the tag

tag_assignments

A polymorphic join row per assignment:

ColumnNotes
tag_idFK to tags
entity_typeuser_table_row, calendar_event, conversation, agent, person, …
entity_idUUID of the tagged entity
assigned_byThe user who applied the tag
assigned_atTimestamp — drives display order (newest first)

Unique on (tag_id, entity_type, entity_id).

Scope

A tag can be org-wide (visible in every workspace) or workspace-scoped (visible only in its own workspace). The workspace_id column distinguishes them:

  • workspace_id IS NULL → org-wide
  • workspace_id = <ws> → workspace-scoped

The settings UI shows a scope badge on each tag, and you can change scope when editing. Org admins / owners control org-scoped tags; any member can manage workspace-scoped tags.

Where tags show up

  • Records — in the Record Detail Sheet header, always editable, via <TagPicker entityType="user_table_row" />.
  • Calendar events — in the Event Detail Sheet, via the same picker with entityType="calendar_event". Event color in the calendar is derived from the most recently assigned tag.
  • Conversations and Agents — tag pickers in their detail views.
  • People — the People table's tags text array overlaps with this system (see Data tables).
  • Filters & pipelines — the pipeline stage widget filters by tag; filter trees support tag conditions.

Two-way record ↔ event sync

When a record is linked to a calendar event (via calendar_event_links), tags sync both ways automatically. Logic lives in src/store/tags.jsx in resolveSyncTargets():

  • Tagging a record fans the tag out to every linked calendar event.
  • Tagging an event fans the tag out to (a) the linked record and (b) every other event linked to that record.

SYNCED_ENTITY_TYPES = { 'user_table_row', 'calendar_event' }. Other entity types (conversation, agent, person) don't fan out — they're self-contained.

One-way record → email inheritance

Emails are tagged independently, but the inbox physically copies a linked record's tags onto an email the first time you open it. The copy runs through the same inheritTagsBetweenEntities action the calendar uses on event-record links — idempotent (skip-existing on the target), so reopening only catches up new tags added to the source record.

Rows land in tag_assignments with entity_type = 'email_message' and the same TagPicker used by the calendar lets you add or remove tags directly on the email afterwards.

The copy is one-way: tagging an email does not fan out to the source record. The semantic owner of the tag is the record; the email gets a snapshot. See Inbox → Tag inheritance for the link rule that drives the sender → record match.

Settings: managing tags

⌘KSettings → Tags opens TagsTab.jsx:

  • Inline editor — name input, color swatch, scope select (Org / Workspace).
  • Drag-and-drop reorder writes to display_order.
  • Delete cascades to all tag_assignments.

A useful pattern: define a small set of org-wide tags for cross-cutting concepts (priority, status, lifecycle stage), then add workspace-scoped tags for project- or client-specific work.

Newest-tag-wins for event color

For calendar event coloring the most recently assigned tag wins. This is intentional — moving an event from "lead" to "customer" by adding a customer tag should change the color even if the older lead tag is still attached.

The mechanic is: tag_assignments.assigned_at is preserved on re-assignment (not silently no-op'd), and the API returns assignments ordered by assigned_at DESC. The calendar's useEventTagColor hook reads the last item in the array, so the newest is what renders.

Where to next

On this page