Emails arrive. Calls end. Chat threads spool past. In every one, someone commits to a date, flags a risk, changes their mind, or names an outcome. The meaning is there for a second, then gone.
Actionary catches that second. An adapter workflow trawls the mail every N minutes, an LLM step classifies what matters, and the result lands in a triage inbox — one signals row per event, ready for a human to accept or dismiss. The classifier proposes rows. A person confirms them. The platform lands them.
This is where noise becomes substrate. Every accepted signal materialises a row the agent can reason over, the workflow engine can trigger from, and the audit log can point at forever.
The triage inbox
The signals feed is a dedicated triage surface. Classifier hits land here for a human to review before any record gets created. Each row on the feed carries a headline, a markdown summary with the meaning phrase highlighted inline, a semantic meaning pill driven by signal_kind_id, and a source badge naming the adapter that authored it — Microsoft 365 Mail, Gmail, Slack, Manual, Meeting Recorder.
The operator accepts or dismisses. Both are idempotent endpoints (POST /api/v1/signals/{id}/accept and .../dismiss); a second call on an already-accepted signal returns 409 with the prior accepted_record_id. Every transition writes one audit_log row — two on Accept, because the created downstream row is audited too.
Five kinds of meaning
signal_kinds is a registered lookup entity seeded with the five categories that carry the load: Decision, Risk, Commitment, Intent, Change. A tenant admin can rename the labels through Schema Admin; the pills on the feed pick up the new wording without a code change, because the pill text reads through the lookup like every other reference table in the platform.
The categories are how the LLM turns a paragraph of prose into a structured claim. “The customer asked for a call next Wednesday” is a Commitment. “They mentioned they are evaluating a second vendor” is an Intent. “The renewal date moved from March to June” is a Change. Five shapes cover the meaning space of a business conversation; the classifier picks one per signal.
Two tiers of prompt governance
The classifier prompt is a first-class configuration surface, governed at two levels. The root admin edits the platform default at /platform/agent; a tenant admin edits the tenant override at /settings/agent. The resolver picks tenant → platform → shipped default at every turn.
Per-user editing does not exist. A tenant’s signals inbox is shared substrate — one bad edit from one user would poison the classifier for the whole workspace. The prompt is a governance surface. Migration 224_signals_prompt_governance.sql splits the columns cleanly across platform_agent_settings and tenant_agent_settings so the two tiers cannot bleed.
Accept, with a picker
Accept opens a dropdown. The signal_action_types lookup enumerates what accepting a signal means — Create Task, Create Contact, Create Deal, Create Interaction, Create Activity, Follow Up Email, Link To Existing Record, None. The operator picks the target from the dropdown before firing Accept; the entity picker beside it names which record to link to when the target is an existing account, contact, or deal.
The handler reads the classifier’s proposed record shape, resolves the target entity from the action type, creates the row through EntityService.create, and in the same transaction stamps accepted_record_entity and accepted_record_id on the signal. The audit trail links both ways: from the signal to the created row, and from the created row to the signal that spawned it.
Idempotent at the boundary
Every adapter that feeds signals carries a source-side identifier — a mail message id, a Slack thread id, a meeting recording id. The signals table declares a uniqueness rule on (tenant_id, signal_source_id, source_external_id) where both source columns are present. An adapter retry that fires the same mail through the workflow twice converges to a single row.
This is the same durable-execution posture the platform holds at every other write boundary — the workflow engine’s durable step claim, the public form endpoint’s Idempotency-Key header, the outbound webhook fan-out’s HMAC-signed retries. A network stutter never turns one email into two triage cards.
Per-user noise floor
Mute rules let each user drop the noise before the classifier ever sees it. Every signed-in user gets a private email_mute_patterns list at /profile/agent — substrings and wildcard rules that drop matches at the connector boundary. When the agent calls the M365 mail search adapter, hits matching any of the caller’s rules are dropped before the tool result is composed. Chat and the signals feed never see them.
The mute list is per-user. Tenant admins cannot see or edit another user’s rules; the noise floor is a personal preference. Storage is one field on the user’s preferences JSON; no new table. Patterns are validated at the provider boundary so a malformed rule cannot break the whole turn.
The affordance-prompt shortcut
The chat surface carries a Get signals button that seeds the composer with the full classifier prompt. The chat bubble on submit shows a short label; the model receives the long prompt. Every user message carries both a text form and a display form; the bubble renders the display form; auto-titling on the conversation prefers it too. The operator sees a clean history. The model sees the whole instruction.
The takeaway
Every business runs on moments that vanish the second they happen. Signals catches those moments as structured substrate — one row per event, five kinds of meaning, human-confirmed materialisation, idempotent by construction. The classifier proposes; the operator lands; the agent reads what the operator confirmed. Meaning becomes data. Data becomes decisions the whole platform can act on. This is why revenue leaders get a pipeline that grows from what actually happened in the field.