ADR-0133shipped
Contact Enrichment Pipeline
Context
Recent contact-prep work exposed the problem: creating a useful contact dossier requires searching 5+ data sources (Slack channels, Slack DMs, Roam archive, web/GitHub, Granola meetings, memory/Qdrant) and synthesizing results into a Vault contact file. Done manually, this is slow, inconsistent, and incomplete.
Contact enrichment should be a durable Inngest pipeline that fans out across all available sources, synthesizes results, and writes/updates the Vault contact file.
Decision
Event: contact/enrich.requested
{
name: "contact/enrich.requested",
data: {
name: string, // "Sample Contact"
vault_path?: string, // "Contacts/Sample Contact.md" (if exists)
hints?: { // Optional known identifiers to seed search
slack_user_id?: string,
github?: string,
twitter?: string,
email?: string,
website?: string,
},
depth?: "quick" | "full", // quick = Slack + memory only, full = all sources
}
}Inngest Function: contact-enrich
Fan-out steps, each independent and failure-isolated:
search-slack— Find user in workspace, map all channels (cc-, lc-, dd-, brain-, DMs), pull recent DM history (last 30 messages)search-slack-connect— Check external/Slack Connect users if not found in workspacesearch-roam— Query Roam EDN archive for person pages, block mentions, relationship tags (collaborator/, client/, staff/)search-web— Web search for name + known identifiers. GitHub profile, personal site, social presence, public projectssearch-granola— Query Granola MCP for meetings involving this person (by name search)search-memory— Qdrant semantic search for observations mentioning this personsearch-typesense— Search indexed content (vault notes, channel messages, transcripts) for mentionssynthesize— Sonnet 4.6 takes all source results + existing contact file (if any), produces updated Vault contact markdown with:- Frontmatter (name, aliases, role, organizations, vip, channel IDs, tags)
- Narrative summary
- Contact channels (all discovered)
- Projects (cross-referenced with ADRs and active work)
- Key context (personality, working style, relationship history)
- Recent activity timeline
write-vault— Write/update~/Vault/Contacts/{name}.mdnotify— If VIP, push summary to Joel via gateway
Flow Control
- Concurrency: 3 (don’t overwhelm APIs)
- Steps 1-7 run in parallel (independent sources)
- Step 8 waits for all source steps
- Throttle Slack API calls (rate limits)
- Throttle Granola transcript fetches (aggressive rate limiting)
Depth Modes
quick (~10s, ~$0.01): Slack + memory only. Good for real-time VIP detection. full (~60s, ~$0.05): All 7 sources + web search + Sonnet synthesis. Good for new contacts or periodic refresh.
Triggers
- Manual:
joelclaw enrich "<contact-name>"(CLI command) - Automatic: When a new VIP DM is detected from an unknown user (ADR-0132)
- Automatic: When
contact/enrich.requestedis fired from any pipeline - Scheduled: Weekly refresh of all VIP contacts (cron)
Skills Required
inngest-durable-functions— function definition, step isolationinngest-steps— parallel fan-out with step.run per sourceinngest-flow-control— concurrency limits, throttlinginngest-events— event schema, fan-out pattern
Consequences
- New contacts go from “who is this?” to full dossier in ~60 seconds
- VIP contacts stay current via weekly refresh cron
- Enrichment quality is consistent — every contact gets the same source coverage
- Each source step can fail independently without blocking others
- Cost is bounded: ~$0.05/full enrichment, ~$3/month for weekly VIP refresh of 15 contacts
- Builds on ADR-0131 (channel intelligence) and ADR-0132 (VIP escalation)
- Person dossier skill (
person-dossier) becomes the manual fallback / template reference