Designing signals
Signals are the moments that make a company worth talking to right now - a raise, a hiring spike, a new CMO - discovered by agents and rendered across seven surfaces in ac-frontend. This page is the design spec for that rendering: the 14-type taxonomy and its color system, the anatomy of a signal card across four densities, the 0-10 lead score scale, the origin tag that marks a pulled signal from a pushed one, and the interaction rules that keep every surface consistent.
The mental model
conceptA signal is intelligence, not interaction history. Activities record what your team did to a company; signals record what the world did to it. That distinction drives every design decision below: signals are discovered by agents, attached to entities, and ranked - never typed into a form as a unit of work.
Sonar and Envoy emit signals during workflow runs with source_agent and workflow_run_id stamped on. The UI is a reading experience first; manual creation exists but is the exception.
One signal can attach to many companies and people through scored join rows. A funding round is one fact even when five tracked contacts work there - surfaces render attachments, not copies.
The lead score and attachment_score decide what surfaces first. Time orders the timeline; relevance orders the launchpad. The same data, two reading orders.
Because signals arrive continuously and unprompted, the UX problem is triage, not entry. Every surface below is an answer to one question: how much signal detail does this moment need - a pill, a row, a card, or a full history?
The taxonomy and its colors
interactive 14 canonical signal_type values, each mapped to one of six pill variants by getSignalTypeVariant(). The variant drives both the pill and the card background, so color always means the same thing. Pick a type to see exactly what the product renders.
'funding'→variant: 'success'→bg-green-50 border-green-200Closed a $14M Series A to fund expansion into the US market.
↗ techcrunch.comFunding - A raise just landed - budget exists and priorities reset.
Users and agents can emit free-form types; an LLM normalizer (ENG-593) maps them onto the canonical 14 so filtering and color stay coherent. The taxonomy is a rendering contract, not a validation wall.
Anatomy of a signal, four densities
interactive One unified shape (UnifiedSignal), one renderer family (SignalCard / CrmSignalItem), four densities. Switch density to see how the same funding signal degrades gracefully; click a numbered part for what it does and why.
"The round was led by a growth fund and will fund US expansion." 4
Full content: pill, date, description, snippet, source. The reading density.
The lead score scale
interactive Signals say what happened; the lead score says how much it matters. An 11-point scale (0-10) collapsed into five labeled tiers (ENG-741), rendered by LeadScoreBadge next to signals on the launchpad and in Sonar results. Drag the slider.
On ranked surfaces the badge sits beside the signal pills and lead_reason explains the number in one line. Fact, weight, and reasoning - never just a number on its own.
Where signals surface
interactiveSix surfaces, one renderer family, density chosen by intent. Pick a surface to see its layout, the component behind it, and the actions it allows.
The "hottest signals" widget above the fold - a ranked mix of both origins from intel_org_signal_feed. Each row carries a small origin tag so the user can tell a signal they pulled (sonar) from one the system pushed (proactive). Company name links into CRM; an "Open Sonar" button appears when the app is installed.
Interaction patterns
behaviorThe behaviors that repeat on every surface - the contract between the signal system and the rest of the product.
Every type pill wraps in an AcHoverCard. The pill stays small for scanning; hovering reveals the full signal - date, description, source - without navigation. Detail on demand, never detail by default.
Row density caps visible pills at 2 and folds the rest into a +N badge whose tooltip lists the hidden type names. A company with 9 signals scans exactly as fast as one with 2.
Delete follows the destructive-action pattern: confirmation alert, soft delete through the store (deleted_at, never a hard drop), success toast. Caches invalidate on every write.
useCrmSignalsStore caches per company with a 30s stale-after TTL and batch-fetches many companies in one request. In-flight fetches cancel via AbortController; no polling, no websockets.
The data layer tab keeps filter state (type, company, search) in a module-scoped composable and syncs it with saved filters, so a triage view survives navigation and sessions.
Signals explain what happened; the lead score badge next to them says how much it matters. The pair turns a feed of facts into a ranked queue of conversations worth starting.
Design rules
do / don'tThe do's and don'ts that keep a new signal surface consistent with the seven that exist.
One color system, all surfaces
signal_type → AcPill variant → card background, always through getSignalTypeVariant() and getSignalCardBg(). A funding signal is green on the launchpad, in the timeline and in the table.
Density follows intent
Row for scanning, card for reading, timeline for history, compact for context. Pick by what the user is doing on that surface, not by available space.
Provenance always visible
Source link, event date and source_agent travel with every signal. A signal without a source reads as a rumor; the UI never renders one without its link when a URL exists.
Label the origin
On the launchpad mix, tag each row sonar (the org pulled it) or proactive (an agent pushed it), read from intel_org_signal_feed.origin. The user should never wonder whether they asked for a signal or the system surfaced it.
Unify before rendering
Every producer shape (relational, Envoy synthetic, legacy JSONB) adapts to UnifiedSignal before it touches a component. One renderer, many sources.
No new colors per surface
Unmapped types (event, news, product, expansion, partnership) fall back to neutral gray. When a type becomes common, extend the variant map - never hardcode a color in a component.
No raw scores
Lead score renders as the 0-10 badge with one of five labels ("Very strong fit"). Never expose a raw float or invent intermediate tiers.
No writes in AI context
The Envoy panel is read-only. Deleting evidence from a drafted email would silently change what the AI claimed to know.
No detail walls
Never render full signal cards in a scanning surface. If a surface shows more than ~3 signals per entity, switch to pills + hover or a +N fold.
Types: src/types/signal.ts · colors and labels: src/helpers/signalHelpers.ts · renderers: SignalCard, CrmSignalItem, SignalTypePill, LeadScoreBadge · state: useCrmSignalsStore · API: /crm/signals. The storage-side view of the same system is documented in the signals tables.