Status: Draft — Under Review Version: 2.3 Created: 2026-03-18 Last Updated: 2026-04-22 Authors: CroohM Product & Engineering Reviewers: TBD Classification: Internal — Confidential
- Abstract
- Tenets
- Problem Statement
- Background & Motivation
- Goals & Success Metrics
- Non-Goals
- Terminology
- System Design
- Technology Stack
- AI Tool Landscape Analysis
- Data Model
- API Design
- Security & Threat Model
- Capacity Planning
- Observability, SLIs, SLOs & Alerting
- Implementation Plan
- Rollout Strategy
- Alternatives Considered
- Risks & Mitigations
- Open Questions
- Decision Log
- References
CroohM is a mobile-first CRM and multi-channel messaging platform serving SMB customers primarily on WhatsApp and Gmail. This RFC proposes evolving CroohM into a Conversational Business Engine: a workspace-scoped AI system where business operators issue natural language instructions and CroohM autonomously executes the work — reading CRM state, drafting messages, sending outreach, updating leads, and running scheduled automations — with guardrails, approvals, and full auditability.
This is not "adding a chatbot." This is a new execution layer that sits between the user's intent and every business operation CroohM currently performs manually. The architecture described here is designed to run on Google Cloud Platform (GCP), use OpenRouter as the primary model routing layer, start with MiniMax as the main agent model, experiment with free and open models for low-risk workloads, and be production-grade from Phase 1. The system is designed to improve over time through explicit feedback, eval replay, routing adjustments, prompt versioning, and skill refinement.
Tenets are ordered. When trade-offs arise, the higher-ranked tenet wins.
-
Tenant isolation is inviolable. No AI action, retrieval, or context leak may cross workspace or tenant boundaries under any circumstance. Correctness over performance. Isolation over convenience.
-
Host logic, not model logic, controls routing. The model never selects the tenant, workspace, channel, sender identity, or approval gate. These are infrastructure decisions, not inference decisions.
-
Approvals before irreversible actions. Any action that sends a message, modifies a customer record, or calls an external API must be approvable or blockable before execution. Drafts are free; sends are guarded.
-
Observable by default. Every agent run produces a complete, structured trace: inputs, tool calls, decisions, outputs, latency, cost. No black-box execution.
-
Cost is a product constraint, not a footnote. Token usage, embedding operations, and infrastructure costs must be modeled per feature before shipping. Unconstrained AI compute is a business risk.
-
Mobile-first UX, not mobile-optional. Approval flows, run status, agent conversations, and workflow monitoring must work on a 375px screen.
Today, a CroohM user who wants to follow up with stale leads must:
- Open CRM → filter by last contact date → scan 40 rows manually
- Open inbox → check each lead's last WhatsApp message
- Decide who needs follow-up → open each record
- Draft a message → copy/paste from a template → personalize manually
- Send via WhatsApp or Gmail → switch apps
- Update lead status in CRM → set a reminder
- Repeat for the next lead
This loop takes 3–8 minutes per lead. A rep with 50 stale leads spends 2.5–6.5 hours on manual mechanical work that generates zero new insight.
- Leads die in the queue. Response time degrades. Conversion drops.
- CRM state decays. Records go stale because updating them costs time that competes with selling.
- Context is lost. Each new conversation starts without historical context because no one has time to read the full thread.
- Operators can't see the problem. Pipeline risk is invisible until deals die.
- Team leads have no signal. No digest, no summary, no alert — just noise.
Gartner estimates that by end of 2026, 75% of B2B customer interactions will be handled by AI agents (up from 30% in 2023). Salesforce (Agentforce), Zoho (Zia Agent Studio with 700+ actions), Freshworks (Freddy AI), and HubSpot (Breeze) have all shipped agent-native CRM features in the past 18 months. CroohM's differentiation is WhatsApp-native + mobile-first + SMB-focused — but only if we ship before this window closes.
Each of the following products is a standalone company with its own pricing, dashboard, and login. None of them talk to each other. CRoohM does all of it — coordinated — from one app on a phone.
| Product | What It Does | CRoohM Equivalent | Current Pricing |
|---|---|---|---|
| Vibe Prospecting | Find leads across 150M+ businesses and 800M+ profiles using 4,000 signals (funding events, hiring, tech stack, intent). Draft personalised outreach. Built on Explorium data. Has a Claude MCP connector. | Dispatch — one feature inside CRoohM | Free tier; paid plans per credits (Explorium: $200/5K credits) |
| ManyChat | Automate conversations on WhatsApp, Instagram, Messenger, SMS. Intent recognition, lead capture, flow builder. AI add-on is an extra $29/month. | Conversation Automation — one capability inside CRoohM | Free (1K contacts); Pro from $15–435/month by contact count; WhatsApp adds $0.02–0.08/conversation |
| Intercom Fin | AI support agent resolving 67% of customer queries autonomously across 40M+ conversations. Knowledge base RAG, multi-language, Procedures for complex flows. Now spun off as fin.ai. | Support Agent — one agent inside CRoohM | $0.99/successful resolution; 50-resolution/month minimum; Intercom platform seat costs separate |
| item.app | "The first agentic CRM." Train AI agents like new hires by writing process documents (Notion-style). Agents run outbound sales, inbound management, pipeline warming autonomously. YC-backed. | Playbook Engine — one engine inside CRoohM | Not publicly listed; enterprise pricing |
| OpenClaw (formerly Moltbot/ClawdBot, renamed Jan 2026) | Open-source personal AI assistant. Connects to WhatsApp, Telegram, Slack, Discord, iMessage, Signal, Teams, Matrix, Google Chat simultaneously. Persistent memory, proactive outreach, shell/browser/calendar control. | The spirit of CRoohM's Business Brain — but single-user, not multi-tenant SaaS | Free, self-hosted |
The insight: Vibe Prospecting is an entire funded company. For CRoohM, it is one feature — Dispatch. That is the scope of what we are building.
The CRM AI market moved from "copilot suggestions" to "agent-executed workflows" as the dominant pattern in 2026:
- Salesforce Agentforce: $540M ARR by Q3 FY2026, 330% YoY growth. Now bundled free into SMB Starter and Pro plans, eliminating the AI paywall for small teams.
- HubSpot Breeze AI: Included at no extra cost across core plans. 279,000+ active customers. Leading SMB acquisition with integrated marketing-sales-service agents.
- Zoho Zia Agent Studio: 700+ configurable actions, agentic workflows across CRM, support, and email.
- Freshworks Freddy AI: Autonomous agents across sales, support, and ITSM.
The window CRoohM is targeting: these platforms are enterprise-first and desktop-first. Their AI agents live inside complex dashboards. None of them are designed to be operated from a phone. None of them are WhatsApp-native. None of them serve a solo founder or a 3-person team that lives in WhatsApp. That gap is CRoohM's market.
Intercom Fin (AI support agent):
- Separates knowledge loading from channel deployment from live conversation ownership — three independent state machines
- Fact-checked 2026: 67% resolution rate, $0.99/resolution, 40M+ conversations handled. Guarantees 50% automation rate (credits back if Fin resolves fewer than 50%)
- Uses per-workspace knowledge indexes for strict RAG isolation
- Heavy Redis for session context caching — keeps LLM calls fast
- Per-workspace rate limiting prevents noisy-neighbor degradation
- The key insight: "AI enabled" ≠ "AI deployed" ≠ "AI trusted on that channel"
item.app (agentic CRM, YC-backed):
- Core mechanism: write a Notion-style process document → item parses it → creates an autonomous agent that executes each step
- Agents trained in: Outbound Sales, Inbound Management, Growing Existing Accounts, Pipeline Warming
- The AI Assistant handles ad-hoc queries: "send follow-ups, research leads, or add deals with a simple prompt — no complex UI, no dropdowns, just ask"
- The key insight: CRoohM's Playbook Engine + AI Assistant must deliver this same "write it like a doc, runs like a workflow" experience
Vibe Prospecting / Explorium:
- Data layer: 146M+ business entities, 767M+ employee profiles, 50+ sources, 4,000+ signals
- Real-time triggers: IPOs, M&As, funding events, tech stack changes, hiring signals (EventAPI)
- MCP server available — Explorium already exposes their data to LLM agents via MCP
- Pricing: $200 for 5K credits; credits expire after 12 months
- The key insight: CRoohM's Dispatch does not need to build the data layer. It integrates with Explorium/Vibe Prospecting's MCP server. The value we add is the context (lead is already in the inbox, conversation history exists) and the execution (draft, approve, send — all inside CRoohM)
OpenClaw (formerly Moltbot/ClawdBot — renamed January 27, 2026):
- Open-source, self-hosted personal AI assistant across 10+ messaging platforms simultaneously
- Proactive: sends you messages first (reminders, alerts, morning briefings) — not just reactive
- Persistent memory: remembers everything across sessions, never repeats context
- Actions: shell commands, browser control, calendar, email — from any chat app
- Key gap vs CRoohM: single-user, no multi-tenant model, no workspace/team concept, no WhatsApp Business API (personal WhatsApp only), no CRM data layer
- The key insight: this is the best reference for CRoohM's Brain UX and memory model. Adapt the proactive surface pattern ("3 leads stuck — want me to follow up?") and the persistent memory design
ManyChat (conversation automation):
- Fact-checked 2026: Pro plan $15–435/month by contact count. WhatsApp is an add-on ($0.02–0.08/conversation, first 1K/month free). AI add-on is an extra $29/month.
- Automation spans Instagram, WhatsApp, Messenger, SMS, email
- Key limitation: does not connect to a CRM — you must push data to an external tool via integration
- The key insight: CRoohM's Conversation Automation beats ManyChat because the data is already in the same system. No Zapier. No Notion database. The conversation, the lead, the history — all one record.
Perplexity Computer (launched Feb 2026):
- General-purpose digital worker orchestrating 19 AI models in a sandboxed compute environment
- Runs a routed multi-model stack and treats model selection as an execution policy
- Priced at $200/month for the Max tier — validates that users pay premium for genuine autonomous execution, not just generation
- The key insight: users pay for outcomes (tasks completed) not for conversations had
OpenRouter + routed provider ecosystems (2026):
- Treats providers and models as a routable layer behind one API surface
- Enables price/performance routing, fallback behavior, and access to both frontier and open models
- The key insight: model policy should be dynamic and evidence-driven, not frozen into the first shipped provider choice
OpenAI Agents SDK (production-stable 2026):
- Lightweight Python + TypeScript framework for multi-agent workflows
- Core primitives: Agent (instructions + tools), Runner (execution loop), Handoff (transfer ownership between agents), Guardrail (input/output validation), Session (persistent memory within an agent loop)
- The key insight: the abstraction that matters is not the model, it's the agent loop with sessions and handoffs
Google Agent Development Kit (ADK, 2026):
- Code-first framework for building agents on GCP (Python + TypeScript SDKs)
- Single command deployment to Cloud Run:
adk deploy cloud_run - Workflow agents for Sequential, Parallel, Loop patterns
- MCP support as client — ADK agents can consume MCP servers
- The key insight: GCP's ADK is the path of least resistance for GCP-native AI services
LangGraph (v1.0 stable, March 2026):
- Graph-based agent orchestration with stateful, durable execution
- Used in production by Klarna, Replit, and others
- Six built-in features: parallelization, streaming, checkpointing, human-in-the-loop, tracing, task queue
- If an agent crashes mid-execution, it resumes from the last checkpoint
- The key insight: durable checkpointing is not optional for long-running business workflows
item.app + Intercom Fin (hands-on product testing, March 18, 2026):
- Item demonstrates the strongest "operator AI" shape: ask for business work, inspect CRM state, invoke actions, create skills, and send real messages through connected tools
- Intercom Fin demonstrates the strongest "channel AI" shape: knowledge ingestion, test harness, staged deploy, channel routing, and live ownership are separate state machines
- The key insight: CRoohM must unify both. Business operators need item-style actionability; customer channels need Fin-style deployment discipline
CroohM already has the substrate that takes competitors years to build:
| Asset | Current State | What It Enables |
|---|---|---|
| Multi-tenant workspace model | ✅ Exists | Natural isolation boundary for AI |
| WhatsApp transport + webhooks | ✅ Exists | Real channel execution without integration work |
| Lead + conversation data model | ✅ Exists | Rich context for AI grounding |
| Interaction logging | ✅ Exists | Foundation for agent run traces |
| Team-based workflow expectations | ✅ Exists | Approval and handoff UX model |
| Mobile-first product posture | ✅ Exists | Differentiated AI UX on mobile |
| GCP infrastructure | ✅ Exists | Native integration with ADK, Cloud Run, Pub/Sub |
What is missing is only the AI execution layer on top of this foundation.
- G1: Build a workspace-scoped conversational agent that executes real business operations in natural language
- G2: Support three agent modes: channel agent (customer-facing), operator agent (internal user), workflow agent (event/scheduled)
- G3: Implement a tool execution framework with approval gates for irreversible actions
- G4: Build a knowledge and retrieval layer with strict workspace-scoped RAG
- G5: Instrument every agent run with full traces, token counts, and cost attribution
- G6: Support multi-channel deployment: WhatsApp, Gmail, web chat
- G7: Make the architecture BYOK-compatible (Bring Your Own Key) for enterprise
Leading indicators (measured during build):
| Metric | Target | Measurement |
|---|---|---|
| Agent task completion rate | ≥ 85% on internal golden test set | Automated eval suite |
| Tool call accuracy | ≥ 90% correct tool + parameters | Eval vs labeled dataset |
| Retrieval faithfulness | ≥ 80% (RAGAS faithfulness score) | RAGAS eval pipeline |
| P95 operator agent response latency | < 4 seconds | Langfuse traces |
| P95 channel agent response latency | < 2 seconds | Langfuse traces |
Lagging indicators (measured post-launch):
| Metric | Target at 90 days | Measurement |
|---|---|---|
| Manual follow-up time per rep | -50% vs baseline | Time-tracking survey |
| Lead response rate | +20% vs control group | CRM conversion funnel |
| CRM record update rate | +35% vs baseline | Firestore write events |
| Workspace AI feature activation rate | ≥ 40% of active workspaces | Feature flag analytics |
| AI credit usage per workspace/month | Avg $8–15 platform cost | Token × model price |
- Not building a general-purpose autonomous shell accessible to end users
- Not exposing arbitrary code execution to business users
- Not supporting all model providers at parity in V1 (OpenRouter-first with controlled routing and provider abstraction)
- Not building a public plugin marketplace in V1
- Not fine-tuning any model in V1
- Not replacing human judgment on high-stakes decisions — AI assists and drafts, humans approve and send
- Not building a no-code workflow builder in V1 — product-defined templates only
| Term | Definition |
|---|---|
| Workspace | A CroohM tenant's operating unit. All agents, channels, and data are workspace-scoped. |
| Agent | An LLM-powered executor with a system prompt, tool access, and a run lifecycle |
| Tool | A typed, schema-defined function the agent can call (read CRM, send WhatsApp, draft email, etc.) |
| Skill | A reusable business capability composed of a system prompt policy + allowed tools + output contract |
| Agent Run | A single execution instance of an agent: input → planning → tool calls → output |
| Run Trace | Structured log of every step in an agent run: inputs, tool calls, LLM responses, latencies, costs |
| Session Lane | A serialized execution context per entity (conversation, lead, workspace operator session) that prevents concurrent conflicting mutations |
| Approval Gate | A pause point before an irreversible action where a human can approve or block execution |
| Handoff | Transfer of conversation ownership from agent to human, or between agents |
| Knowledge Source | A document, website, snippet, or CRM object indexed for retrieval |
| RAG | Retrieval-Augmented Generation — retrieving relevant context chunks before LLM generation |
| BYOK | Bring Your Own Key — customer provides their own LLM provider API keys |
| MCP | Model Context Protocol — an open standard for connecting AI models to external tools and data |
| SLI | Service Level Indicator — the measured metric |
| SLO | Service Level Objective — the target for the SLI |
graph TB
subgraph Surfaces["CroohM Surfaces"]
MA[Mobile App<br/>React Native / Expo]
WA[Web App]
WP[WhatsApp<br/>Meta Cloud API]
GM[Gmail]
WC[Web Chat]
end
subgraph Gateway["API Gateway — Go / Cloud Run"]
GW[croohm-api<br/>Auth · JWT · IAP · Rate Limit<br/>Inject tenantId + workspaceId]
end
subgraph Orchestrator["Orchestration Layer — TypeScript / Cloud Run"]
direction TB
RR[Request Router<br/>Deserializes intent]
CC[Context Engine<br/>CRM · Memory · Knowledge · Config]
PE[Policy & Approval Engine<br/>Guardrails · Tool allowlist]
AR[Agent Runtime<br/>LangGraph.js + OpenRouter tool-use loop]
SL[Session Lane Manager<br/>Redis distributed locks]
end
subgraph Tools["Tool Runtime — Typed dispatch"]
CT[CRM Tools<br/>read_leads · update_lead]
MT[Message Tools<br/>draft_whatsapp · send_whatsapp<br/>draft_gmail · send_gmail]
WT[Workflow Tools<br/>trigger_workflow · schedule_callback]
end
subgraph Workers["Realtime Worker Layer — Cloud Run + Pub/Sub"]
EW[Event Workflow Worker]
AW[Approval Resume Worker]
SD[Send Dispatcher<br/>idempotent external actions]
CS[Context Summarizer]
end
subgraph Batch["Batch Worker Layer — Cloud Run Jobs"]
EP[Embedding Pipeline]
ER[Eval Runner]
AA[Audit Archiver]
RS[Retention Sweeper]
end
subgraph Storage["Storage Layer"]
FS[(Firestore<br/>Leads · Convs · Agents<br/>Runs · Approvals)]
VS[(Vector Store<br/>Workspace namespaces<br/>Knowledge Index)]
RD[(Redis<br/>Session locks<br/>Rate limit counters)]
GCS[(Cloud Storage<br/>Docs · Trace archives)]
end
subgraph LLM["LLM Provider — via OpenRouter"]
OR[OpenRouter<br/>MiniMax primary · OSS fallback<br/>Route policy versioned]
end
Surfaces --> Gateway
Gateway --> Orchestrator
Orchestrator --> Tools
Orchestrator --> Workers
Workers --> Batch
Orchestrator <--> Storage
Tools <--> Storage
Workers <--> Storage
Batch <--> Storage
AR <-->|tool-use loop| OR
- Language: Go
- Platform: Cloud Run (min instances: 2, max: 100)
- Responsibilities:
- Authenticate requests (Firebase Auth / Identity Platform JWT validation)
- Enforce Google IAP for internal service-to-service traffic
- Extract and inject
tenantId+workspaceIdinto every downstream request header — nothing downstream trusts the request body for these values - Per-workspace rate limiting: token bucket in Redis, 1000 req/min default, configurable per plan
- Route to orchestration, connector, or standard CRUD handlers
- Expose approval APIs and run inspection APIs to the mobile/web clients
- Language: TypeScript
- Platform: Cloud Run (min instances: 1, max: 50)
- Framework: LangGraph.js for stateful agent loops with checkpointing
- Responsibilities:
- Assemble the composed run context (8 layers — see Section 8.4)
- Classify request: channel agent, operator agent, or workflow agent
- Execute the tool-use loop via OpenRouter with provider-aware routing and streaming
- Manage session lanes — acquire a distributed lock per
(workspaceId, sessionBoundaryId)before any mutation-capable run - Enforce approval gates — pause run, emit
ApprovalRequestto Firestore, wait for approval webhook before continuing - Emit structured run trace to Langfuse on every step
- Handle handoffs: agent-to-human (update conversation
handoffState), agent-to-agent (spawn sub-run)
Agent Tool-Use Loop (OpenRouter):
User Request
│
▼
[System Prompt Assembly]
Platform policy + Workspace config + User role + Object context
+ Conversation memory + Knowledge retrieval results + Run scratchpad
│
▼
[Model Inference — routed via OpenRouter]
Default: MiniMax for orchestration
Fallback: OSS/free or alternate providers based on route policy
Tool use mode: tools = workspace_allowed_tools filtered by risk policy
│
├──► [Tool Call] → Tool Runtime → Result → back to model
├──► [Tool Call] → Approval Gate → wait → resume or abort
└──► [Final Response] → Channel Formatter → User / Channel
Prompt Caching Strategy:
- Static prefix (platform policy + workspace config + user role) → cached with
cache_control: ephemeral - Dynamic suffix (object context + retrieval results + conversation) → not cached
- Expected cache hit rate: 60–70% of tokens on repeat operator sessions → ~50% cost reduction on those sessions
Routing Policy Strategy:
- Route selection is host-controlled and versioned
- Initial policy:
- MiniMax via OpenRouter for planner/executor paths
- low-cost OSS/free models for classification, extraction, and summarization after eval approval
- premium fallback route reserved for failures, high-stakes runs, or regression recovery
- Every run persists:
provider,model,routePolicyVersion,credentialProfileId,fallbackUsed
Five retrieval operations assembled before every run:
- CRM Object Context — fetch lead/conversation/task from Firestore by ID
- Conversation Memory — fetch
ContextSummaryfor the session (short summary, working summary, extracted facts, open loops) - Knowledge Retrieval — vector search against workspace's knowledge index with hard filters on
workspaceId+visibility - Workspace Config — agent persona, brand voice, operating rules, approval policies
- User Role + Permissions — what tools this user is allowed to invoke
All five are assembled in parallel (Promise.all). Total context assembly target: < 300ms P95.
Each tool is a typed, schema-validated function registered in the ToolRegistry. Every tool definition includes:
interface ToolDefinition {
name: string;
description: string; // shown to the model
inputSchema: JSONSchema;
outputSchema: JSONSchema;
riskLevel: ToolRisk; // "read" | "draft" | "write" | "external_send" | "admin"
requiredPermissions: string[];
requiredConnector?: string; // "whatsapp" | "gmail" | null
approvalBehavior: ApprovalMode;
rateLimitPerWorkspace?: number; // calls/min
observabilityTags: string[];
}Tool execution is always:
- Permission check against user role
- Approval gate check against workspace policy
- Acquire session lane lock if mutation
- Execute
- Emit tool call span to run trace
- Release lock
- Language: TypeScript
- Platform: Cloud Run
- Responsibilities:
- Gmail: OAuth callback handling, Gmail watch setup (Pub/Sub push), thread-to-conversation mapping, draft and send via Gmail API
- WhatsApp: Meta Cloud API webhook ingestion, template sync, 24-hour window enforcement, approved template fallback
- Web chat: WebSocket or SSE channel for real-time chat widget
- Language: TypeScript + Python
- Platform: Cloud Run services triggered by Pub/Sub and Cloud Tasks
- Responsibilities:
- Event Workflow Worker: Execute low-latency event-triggered automations (e.g. inbound WhatsApp → qualify → draft → approval)
- Approval Resume Worker: Resume paused runs after approval or rejection
- Send Dispatcher: Commit prepared external actions with idempotency keys and webhook correlation
- Context Summarizer: Sliding-window conversation summarization to keep context compact
- Language: Python
- Platform: Cloud Run Jobs (triggered by Cloud Scheduler and explicit admin actions)
- Responsibilities:
- Embedding Pipeline: Chunk knowledge sources, generate embeddings, write to vector index
- Eval Runner: Nightly runs of the agent eval suite, write results to Firestore
eval_results - Audit Archiver: Flush completed run traces from Firestore to Cloud Storage as Parquet for long-term storage and cost control
- Retention Sweeper: Execute TTL deletion, workspace erasure, and vector cleanup jobs
graph TD
T[Tenant] --> W1[Workspace A<br/>Sales Team]
T --> W2[Workspace B<br/>Support Team]
T --> W3[Workspace N<br/>...]
W1 --> WA1[WorkspaceAgent<br/>channel / operator / workflow]
W1 --> CD1[ChannelDeployments<br/>WhatsApp number · Gmail · Web chat]
W1 --> WD1[WorkflowDefinitions<br/>triggers + steps]
W1 --> KS1[KnowledgeSources<br/>docs · website · snippets]
W1 --> CP1[CredentialProfiles<br/>platform-managed or BYOK]
W1 --> U1[Users]
WA1 --> SK1[SkillDefinitions<br/>qualify_lead · follow_up_pipeline]
WA1 --> TA1[ToolAllowlist<br/>per-agent tool permissions]
U1 --> UR1[Owner<br/>all permissions + approval authority]
U1 --> UR2[Sales Rep<br/>assigned queues + draft/send]
U1 --> UR3[CSM<br/>read + draft only]
style T fill:#f5f5f5,stroke:#333
style W1 fill:#dbeafe,stroke:#2563eb
style W2 fill:#dbeafe,stroke:#2563eb
style W3 fill:#dbeafe,stroke:#2563eb
Key decisions:
- Channels are workspace assets, not user assets
- Agent behavior (persona, tools, approval policy) is workspace-configured
- User overlays add: role-based permissions, personal defaults, optional personal memory
- A workspace cannot see or act on another workspace's data — enforced at the API gateway (tenantId/workspaceId injection) and at every Firestore query and vector retrieval
Every agent run composes context from exactly these 8 ordered layers:
Layer 1: Platform Policy ← hardcoded, never tenant-overridable
Layer 2: Tenant Policy ← from Firestore TenantConfig
Layer 3: Workspace Config ← WorkspaceAgent + CredentialProfile + brand voice
Layer 4: User Role + Permissions ← from Firestore UserRecord
Layer 5: Object Context ← Lead | Conversation | Task | Deal fetched live
Layer 6: Conversation Memory ← ContextSummary (summary + facts + open loops)
Layer 7: Knowledge Retrieval ← top-K chunks from vector store, workspace-scoped
Layer 8: Run Scratchpad ← tool call results accumulated during this run
Layer 1 is always prepended and cannot be overridden by any workspace. This includes the tenant/workspace isolation instructions and approval enforcement rules.
Concurrent conflicting mutations are one of the highest-risk failure modes in agent systems (duplicate sends, conflicting lead updates, double booking). CroohM serializes runs per session lane using distributed locks in Redis.
Session lane boundaries:
| Session Type | Lane Key |
|---|---|
| Customer-facing conversation | conv:{conversationId} |
| Lead-scoped operator run | lead:{workspaceId}:{leadId} |
| Workflow background run | wf:{workflowRunId}:{targetId} |
| Workspace operator session | ops:{workspaceId}:{userId}:{sessionId} |
- Lock TTL: 120 seconds (auto-released on run completion or timeout)
- Lock acquisition timeout: 5 seconds (fast fail — return "agent busy on this entity" error)
- Lock owner: agent run ID (enables detecting stale locks from crashed runs)
Important rule: session lane locks protect active mutation windows, not multi-hour approvals. A run must release the active lock before waiting on human approval, then reacquire it for the final commit step.
All side-effecting actions use a three-stage protocol:
- Prepare
- The agent computes intent and produces a normalized action payload
- The tool runtime validates permissions, policy, and connector readiness
- A
PreparedActionrecord is written with:preparedActionIdrunIdsessionLaneKeytoolNametargetObjectIdidempotencyKeyproviderPayloadstatus = "prepared"
- Approve / Reject
- If policy requires approval, an
ApprovalRequestreferences thePreparedAction - The active session lock is released while waiting
- Approval callback carries a one-time approval token and the prepared action ID
- If policy requires approval, an
- Commit
- The resume worker reacquires the session lane lock
- Re-validates entity state, connector state, and approval freshness
- Executes the external send or mutation using the original
idempotencyKey - Marks
PreparedAction.status = "committed"or"aborted"
sequenceDiagram
participant Agent
participant ToolRuntime
participant Firestore
participant User
participant ResumeWorker
participant Connector
Agent->>ToolRuntime: prepare send_whatsapp / send_gmail
ToolRuntime->>Firestore: write PreparedAction(idempotencyKey, status=prepared)
ToolRuntime->>Firestore: write ApprovalRequest(preparedActionId)
ToolRuntime-->>User: approval needed
User->>Firestore: approve or reject
ResumeWorker->>Firestore: load PreparedAction + approval
ResumeWorker->>ResumeWorker: reacquire session lane lock
ResumeWorker->>Connector: commit with idempotencyKey
Connector-->>ResumeWorker: provider message id / result
ResumeWorker->>Firestore: mark committed, attach provider ids
Why this matters:
- duplicate approval taps must not create duplicate external sends
- expired approvals must not revive stale drafts
- resumed runs must be replay-safe after crashes
Every external action must persist:
idempotencyKeysendRequestIdproviderMessageIdif returnedproviderThreadIdif relevantdeliveryStateVersion
Rules:
- Gmail and WhatsApp send tools always execute through
PreparedAction - repeated approval callbacks for the same prepared action are ignored after first commit
- duplicate provider webhooks are deduplicated by
(provider, providerMessageId, status, timestampBucket) - status transitions are monotonic:
prepared -> committed -> accepted -> delivered/read/failed
- any non-monotonic or duplicate transition is stored as a no-op audit event, not applied to user-visible state
CroohM explicitly separates low-latency event handling from heavy background work.
| Execution Class | Platform | Examples |
|---|---|---|
| Realtime | Cloud Run services + Pub/Sub + Cloud Tasks | inbound message handling, approval resume, send commit, task creation |
| Batch | Cloud Run Jobs + Scheduler | nightly evals, embeddings backfills, archive export, retention sweeps |
This split prevents implementers from accidentally routing interactive channel work through batch infrastructure.
sequenceDiagram
participant U as User (Mobile)
participant P as Planner Agent<br/>(OpenRouter: MiniMax)
participant O as Orchestrator
participant E1 as Executor Sub-agent<br/>Lead L1
participant E2 as Executor Sub-agent<br/>Lead L2
participant EN as Executor Sub-agent<br/>Lead LN
participant AG as Approval Gate
participant WA as WhatsApp Connector
U->>O: "Follow up all hot leads with no reply in 48h"
O->>P: build execution plan
P-->>O: [{leadId: L1}, {leadId: L2}, ..., {leadId: LN}]
par Fan-out to N executors in parallel
O->>E1: read context + draft message for L1
O->>E2: read context + draft message for L2
O->>EN: read context + draft message for LN
end
E1-->>O: {draft, approvalRequestId}
E2-->>O: {draft, approvalRequestId}
EN-->>O: {draft, approvalRequestId}
O->>AG: write N ApprovalRequests to Firestore
AG->>U: FCM push: "20 follow-up drafts ready for review"
U->>AG: approve all / review / reject per lead
par Dispatch approved sends
AG->>WA: commit send L1 (idempotencyKey)
AG->>WA: commit send L2 (idempotencyKey)
AG->>WA: commit send LN (idempotencyKey)
end
WA-->>O: provider message IDs
O->>U: "20 follow-ups sent. 2 skipped (rejected)."
Orchestration paths:
| Complexity | Path |
|---|---|
| Simple read/summary | Single agent + tool loop |
| Drafting | Single agent + draft tool (no approval needed) |
| Write or external send | Planner → Executor → Approval Gate |
| Customer-facing reply | Executor → Channel Formatter → send or approval |
| Bulk operation | Planner → N parallel Executor sub-agents → aggregate |
The following are never decided by the model. They are injected by infrastructure and are immutable during a run:
tenantIdworkspaceId- sender identity (which phone number, which Gmail account)
- delivery channel
- approval requirement
- escalation target
- tool allowlist (filtered before being sent to the model)
Violation of this principle is a critical security defect, not a product bug.
| Service | Language | Rationale |
|---|---|---|
croohm-api (gateway) |
Go | Goroutine-based concurrency for high-throughput routing; single binary; fast cold start on Cloud Run; strong standard library for auth + HTTP |
croohm-orchestrator |
TypeScript / Node.js | OpenRouter-compatible SDK usage + LangGraph.js are TypeScript-native; async I/O is ideal for LLM API calls (mostly waiting); same language as web app frontend → shared types |
croohm-connectors |
TypeScript / Node.js | Gmail API + WhatsApp API SDKs have strong TypeScript support; WebSocket handling; event-driven pattern |
croohm-workers |
Python | Best ML/embedding ecosystem; sentence-transformers, RAGAS, DeepEval are Python-native; eval tooling is Python-dominant |
| Mobile App | React Native / Expo | Existing stack (per croohm-mobile directory) |
Why not Python for orchestration? Python's GIL limits true parallelism for concurrent agent sessions. At 50 concurrent workspace sessions, TypeScript's event loop significantly outperforms Python asyncio for I/O-bound LLM API calls. Go was considered but LangGraph.js and the JavaScript model-routing ecosystem make TypeScript the pragmatic choice for the orchestration layer.
Why not a single language? Python must be used for the embedding pipeline — no production-grade alternative exists. Go must be used for the API gateway — TypeScript's startup time and memory overhead under load is a meaningful cost at 100+ instances. TypeScript is optimal for orchestration. Polyglot with clear service boundaries is the right trade-off here.
Before describing the routing architecture, it is important to be precise about what "self-hosted" means in different contexts, because the terms are used loosely in the industry and conflating them leads to wrong architectural decisions.
graph TD
subgraph A["Option A: Hosted Gateway (OpenRouter)"]
direction LR
A1[CRoohM Orchestrator<br/>on GCP Cloud Run] -->|HTTPS prompt| A2[OpenRouter<br/>hosted by OpenRouter Inc.]
A2 -->|HTTPS prompt| A3[LLM Provider<br/>MiniMax / Anthropic / Google]
A3 -->|response| A2
A2 -->|response| A1
end
subgraph B["Option B: Self-Hosted Gateway (LiteLLM on GCP)"]
direction LR
B1[CRoohM Orchestrator<br/>on GCP Cloud Run] -->|HTTPS prompt| B2[LiteLLM Proxy<br/>on GCP Cloud Run — YOUR infra]
B2 -->|HTTPS prompt| B3[LLM Provider<br/>MiniMax / Anthropic / Google]
B3 -->|response| B2
B2 -->|response| B1
end
subgraph C["Option C: Self-Hosted Model (GPU VM)"]
direction LR
C1[CRoohM Orchestrator<br/>on GCP Cloud Run] -->|HTTPS prompt| C2[Your GPU VM<br/>running Llama / Mistral / Qwen<br/>model weights on your hardware]
C2 -->|response| C1
end
style A fill:#bbf7d0,stroke:#16a34a
style B fill:#dbeafe,stroke:#2563eb
style C fill:#fef9c3,stroke:#ca8a04
Option A — Hosted Gateway (OpenRouter): CRoohM's orchestrator sends prompts to OpenRouter's servers. OpenRouter routes the prompt to the chosen LLM provider (e.g. MiniMax). The model runs at MiniMax's data center. Your prompt passes through two third-party systems: OpenRouter and MiniMax.
Option B — Self-Hosted Gateway (LiteLLM on your Cloud Run): You run the routing proxy yourself inside GCP. The model still runs at Anthropic's / MiniMax's / Google's data center — nothing changes there. Your prompt passes through only one third-party system: the final LLM provider. The gateway layer is on your infra.
Option C — Self-Hosted Model (GPU VM running Llama/Mistral): You download open-source model weights and run the actual inference on your own GPU hardware. Prompts never leave your infrastructure. This requires GPU VMs (A100/H100 class), significant MLOps work, and is only practical with open-weight models (Llama 3, Mistral, Qwen, Phi, etc.). Enterprise pricing.
CRoohM V1 uses Option A. Option B is the upgrade path for enterprise data residency requirements. Option C is not in scope.
| Gateway | Type | Models Available | Key Advantage | Key Limitation | Data Path |
|---|---|---|---|---|---|
| OpenRouter | Hosted | 300+ models, 50+ providers including MiniMax, Claude, Gemini, Llama, free models | Zero ops, MiniMax available, free model tier, OpenAI-compatible API | Prompts pass through OpenRouter's servers before reaching the LLM provider | Your cloud → OpenRouter (US) → LLM provider |
| LiteLLM | Self-hosted (you run it on Cloud Run) | Same 100+ providers as OpenRouter, OpenAI-compatible API | Prompt never leaves GCP — only goes to the final LLM provider. Full control over logs, caching, routing logic. | Adds a service to operate (Docker, Redis, Postgres). You own uptime. | Your cloud → LiteLLM on GCP → LLM provider |
| Portkey | Hosted or self-hosted | 200+ LLMs, OpenAI-compatible | Enterprise observability, semantic caching, guardrails baked in | Hosted version has same data residency concern as OpenRouter | Your cloud → Portkey → LLM provider |
| Google Vertex AI | GCP-managed | Gemini, Claude (via Anthropic partnership), Mistral, Llama | Fully GCP-native, managed infra, data stays in GCP region, no extra proxy hop | MiniMax not available; locks you into GCP's model catalogue | Your cloud → Vertex (same GCP region) → model |
| AWS Bedrock | AWS-managed | Claude, Titan, Mistral, Llama, Cohere | AWS-native, data residency in AWS region | Not relevant if CRoohM is on GCP; MiniMax not available | Your cloud → Bedrock (AWS) → model |
| Direct provider APIs | N/A (no proxy) | One provider per integration (Anthropic API, OpenAI API, etc.) | Simplest setup, no proxy in the chain | Must write and maintain separate SDK integrations per provider; routing logic lives in your orchestrator code | Your cloud → LLM provider directly |
Why OpenRouter is selected for V1 over the alternatives:
-
MiniMax availability: The RFC's primary model choice (MiniMax) is available on OpenRouter. It is not on Vertex AI or Bedrock. Going direct to MiniMax's API is an option but loses the routing abstraction.
-
Free model tier: OpenRouter operates a free model router — eligible open models can be called at $0 token cost for low-risk workloads (classification, summarization, intent detection). This directly enables CRoohM's $8–15/workspace/month cost target.
-
Routing abstraction: The orchestrator calls one endpoint with one auth header. Model selection, fallback ordering, and provider failover are controlled by the
RoutePolicyrecord in Firestore — no orchestrator code changes when a route policy is updated. -
300+ models in one API surface: MiniMax today, Claude for high-stakes tasks tomorrow, a free OSS model for classification next week — all through the same API call shape.
Why LiteLLM is the V2 upgrade path (not V1):
LiteLLM is the right choice when enterprise customers raise data residency concerns — specifically, the question "does our customer data (lead names, conversation content in prompts) pass through a US-based third party's servers?" With OpenRouter, the answer is yes. With LiteLLM self-hosted on GCP in the customer's preferred region, the answer is no.
The switch from OpenRouter to LiteLLM requires:
- deploying a LiteLLM Docker container to Cloud Run (one-time)
- updating the
OPENROUTER_BASE_URLenv var in the orchestrator to point at LiteLLM instead - migrating
RoutePolicyrecords to use LiteLLM route format
That is a configuration change, not an architecture change. The orchestrator code does not change because both OpenRouter and LiteLLM expose an OpenAI-compatible API.
sequenceDiagram
participant MA as Mobile App
participant GW as API Gateway (Go)
participant OR as Orchestrator (TypeScript)
participant RP as RoutePolicy (Firestore)
participant ORouter as OpenRouter<br/>(or LiteLLM in V2)
participant LLM as LLM Provider<br/>(MiniMax / Anthropic / etc.)
MA->>GW: POST /agent/run {message, contextObjectId}
GW->>OR: forward + inject {tenantId, workspaceId}
OR->>RP: load RoutePolicy for useCase="planner"
RP-->>OR: {primaryRoute: "openrouter:minimax", fallback: ["openrouter:free"]}
OR->>OR: assemble context (8 layers)<br/>build system prompt + tools + user message
OR->>ORouter: POST /chat/completions<br/>Authorization: Bearer OPENROUTER_KEY<br/>model: "minimax/minimax-01"<br/>tools: [...workspace_allowed_tools]
ORouter->>LLM: forward to MiniMax API<br/>(OpenRouter handles auth + routing)
LLM-->>ORouter: {tool_call: "read_lead", arguments: {leadId}}
ORouter-->>OR: stream tool call delta
OR->>OR: execute tool: read_lead(leadId)<br/>scoped to {tenantId, workspaceId}
OR->>ORouter: POST /chat/completions (tool result appended)
ORouter->>LLM: next inference step
LLM-->>ORouter: {content: "Here are the 5 leads..."}
ORouter-->>OR: stream final response
OR-->>MA: SSE events: token stream + run_complete
Every agent capability has a named route policy stored in Firestore. The orchestrator reads the policy at run time — it never hardcodes a model name.
// RoutePolicy document in Firestore
{
id: "planner_v3",
useCase: "planner",
primaryRoute: "openrouter:minimax/minimax-01",
fallbackRoutes: [
"openrouter:anthropic/claude-3-haiku", // if MiniMax is degraded
"openrouter:google/gemini-flash-1.5" // if Claude is also degraded
],
evalVersion: "eval_2026_04_10",
status: "active"
}
{
id: "classification_v2",
useCase: "classification",
primaryRoute: "openrouter:free", // free model router
fallbackRoutes: ["openrouter:minimax/minimax-01"],
evalVersion: "eval_2026_04_08",
status: "active"
}Rules:
- Route changes require replay against the failure bucket eval suite before promotion to
status: "active" openrouter:freeis only permitted foruseCasevalues in:["classification", "extraction", "summarization"]external_sendandadmintool calls may only execute on routes withevalVersiondated within 30 days- A route degradation event (OpenRouter reports provider down) automatically promotes the first fallback and logs a
RouteFailoverEvent
| Use Case | V1 Primary Route | V1 Fallback | Why |
|---|---|---|---|
| Operator agent orchestration (planner + executor) | openrouter:minimax/minimax-01 |
openrouter:anthropic/claude-3-haiku |
Strong reasoning, tool-use accuracy, cost-effective |
| Channel agent responses | openrouter:minimax/minimax-01 |
openrouter:anthropic/claude-3-haiku |
One strong default until live evals justify divergence |
| Intent classification | openrouter:free |
openrouter:minimax/minimax-01 |
Low stakes, simple binary/categorical output, free is sufficient |
| Lead summarization | openrouter:free |
openrouter:minimax/minimax-01 |
Low stakes, no external send |
| Knowledge retrieval judge | openrouter:minimax/minimax-01 |
none | Faithfulness check before customer-facing response |
| Embeddings | Single provider, fixed per env | — | Mixed embedding spaces break vector search |
| Web-grounded research (Phase 5) | Perplexity Sonar API | — | Best-in-class search grounding, separate from action loop |
Cost levers:
- Prompt caching on workspace config + platform policy prefix (expected 60–70% cache hit rate on repeat sessions)
- Free model route for classification before invoking the expensive planner
- Context compaction:
ContextSummaryreplaces raw conversation history beyond a sliding window - Route tuning based on task completion rate and correction rate — not raw token cost alone
CroohM agents must improve continuously. This is a closed-loop system:
- collect explicit feedback: approve, reject, thumbs up/down, edited-before-send
- collect implicit feedback: customer replied positively, customer ignored, user rewrote draft, tool retried, human took over
- run nightly replay against recent failures and golden datasets
- adjust prompts, skills, tool descriptions, retrieval settings, and route policy based on measured outcomes
flowchart LR
PR["Production Runs"] --> FB["User + System Feedback"]
PR --> TR["Run Traces"]
FB --> EV["Eval + Review"]
TR --> EV
EV --> PV["Prompt Versions"]
EV --> SV["Skill / Tool Updates"]
EV --> RV["Route Policy Updates"]
EV --> KV["Knowledge Tuning"]
PV --> NX["Next Deployable Agent Config"]
SV --> NX
RV --> NX
KV --> NX
Required failure buckets:
- wrong tool selected
- wrong target object
- bad retrieval or missing source
- hallucinated answer
- poor channel formatting
- weak personalization
- approval-worthy action missed
No route, prompt, or skill change should be promoted without replaying those buckets.
Primary: LangGraph.js (TypeScript)
LangGraph reached v1.0 stable with 38M monthly downloads (March 2026). It is used in production by Klarna and Replit. Its six built-in primitives — parallelization, streaming, checkpointing, human-in-the-loop, tracing, and task queue — are all required for CroohM's agent platform.
Key LangGraph features we depend on:
- Checkpointing: If an orchestrator crashes mid-run, the run resumes from the last checkpoint on restart — critical for approval-gate workflows that can take hours
- Human-in-the-loop: Native support for pausing execution pending human approval — maps directly to our
ApprovalRequestmodel - Streaming: Real-time token streaming to mobile client via SSE
- Parallelization: Concurrent sub-agent fan-out for bulk operations
Alternative considered: Temporal
Temporal provides durable execution at a higher infrastructure level (separate Temporal cluster). It is the right choice for workflows measured in hours/days. For CroohM V1, LangGraph's native checkpointing in Firestore is sufficient and reduces operational complexity. Temporal is the V2 upgrade path when workflow durations exceed 30 minutes regularly.
Alternative considered: Google ADK
Google ADK (TypeScript) is compelling on GCP. Its adk deploy cloud_run single-command deployment is excellent. However, ADK is Gemini-optimized and ties orchestration more tightly to Vertex AI. CroohM's primary routing layer is OpenRouter and the runtime must stay model-agnostic, so LangGraph gives us more control over the tool-use loop. ADK is worth revisiting if we add Gemini-optimized workflows in V2.
Alternative considered: OpenAI Agents SDK (TypeScript)
Production-stable, provider-agnostic, excellent primitives (Agent, Runner, Handoff, Guardrail, Session). The handoff model maps cleanly to agent-to-human escalation. However, LangGraph's graph model is better suited to CroohM's complex multi-step workflow needs (parallel fan-out, conditional branching, long-running pauses). OpenAI Agents SDK is better for simpler linear agent flows.
Recommendation: shared vector infrastructure with strict namespace isolation
Rationale:
- V1 does not need one physical vector database per workspace
- every chunk must include
tenantId,workspaceId,sourceId, andvisibility - every retrieval query must enforce tenant/workspace filters before any semantic result is used
- the security model relies on CroohM-controlled filters, audit logs, and delete behavior, not vendor marketing claims
- dedicated enterprise indexes can be added later for residency or compliance needs
Migration path:
- V1: managed shared vector backend with strict namespace filtering and deletion support
- V2: dedicated enterprise indexes or self-hosted isolation mode where required
Rejected as default: per-workspace physical vector stores — operationally expensive and unnecessary for most SMB workloads.
Hard requirement regardless of vendor: filtered retrieval, namespace isolation, deletion support, and query auditability.
Firestore for all application state (existing infrastructure):
- AgentRun, AgentRunStep, ApprovalRequest, WorkspaceAgent, ContextSummary, etc.
- Multi-named databases: one Firestore database per environment (prod, staging, dev)
- Document-level security rules enforce workspace isolation as a defense-in-depth measure
- Real-time listeners for approval status polling on mobile (no polling needed — Firestore pushes updates)
Firestore is the right choice here because:
- CroohM already operates on Firestore — no migration cost
- Real-time listeners are critical for approval flow UX (user must see "approved" update instantly on mobile)
- Schema flexibility is valuable for the evolving agent run step structure
- Firestore scales automatically — no capacity planning needed
| Component | GCP Service |
|---|---|
| Container runtime | Cloud Run (API, Orchestrator, Connectors) |
| Batch jobs | Cloud Run Jobs |
| Task queue | Cloud Tasks (guaranteed delivery + retry) |
| Event bus | Pub/Sub (Gmail watch → Connector; Connector → Orchestrator) |
| Scheduler | Cloud Scheduler (workflow triggers) |
| Secret storage | Secret Manager (model API keys, connector OAuth tokens) |
| Key management | Cloud KMS (envelope encryption for secrets in Firestore) |
| Object storage | Cloud Storage (knowledge docs, run trace archives, eval datasets) |
| Cache + locks | Memorystore for Redis (session lane locks, rate limit counters, context cache) |
| Monitoring | Cloud Monitoring + Cloud Trace + Error Reporting |
| LLM observability | Langfuse (self-hosted on Cloud Run) |
| Container registry | Artifact Registry |
| Auth | Firebase Auth / Identity Platform |
| Push notifications | Firebase Cloud Messaging (approval request notifications to mobile) |
Langfuse (self-hosted on Cloud Run, backed by Postgres on Cloud SQL):
- Every agent run emits a Langfuse trace with spans for: context assembly, each LLM call, each tool execution, approval gate events
- Captures: input tokens, output tokens, cached tokens, model, latency per span, total cost
- LLM-as-judge eval pipeline runs nightly: scores run traces on task completion, faithfulness, hallucination
- Prompt versioning: all system prompts version-controlled in Langfuse prompt management
- Zero data residency concern: self-hosted, all trace data stays on GCP
CroohM should build an MCP server exposing CroohM capabilities to the broader agent ecosystem. This serves two purposes:
- Developer tooling: Engineers using MCP-capable clients can interact with CroohM data during development and debugging without leaving the terminal
- Ecosystem reach: Any MCP-compatible client can connect to CroohM's tools — turning CroohM into an AI-accessible business platform, not just an app
Proposed CroohM MCP Server tools:
// Read tools
list_leads(workspaceId, filters) → Lead[]
get_lead(leadId) → Lead + conversation history
get_pipeline_summary(workspaceId) → PipelineSummary
search_knowledge(workspaceId, query) → KnowledgeChunk[]
// Draft tools (no approval needed)
draft_whatsapp_message(leadId, context) → MessageDraft
draft_gmail_reply(conversationId, context) → EmailDraft
// Write tools (approval required)
update_lead_status(leadId, status, notes) → Lead
send_whatsapp_message(leadId, messageId) → SendResult
send_gmail(conversationId, draftId) → SendResultMCP server deployment: Cloud Run service behind IAP, with OAuth 2.0 scoped to workspace access.
OpenRouter is not just a model marketplace for CroohM. It acts as the provider control plane:
- one auth and routing layer for multiple model providers
- route ordering and fallback can be changed without modifying the tool runtime
- free/open models can be introduced gradually behind eval gates
- cost, latency, and success rate can be tracked at the route-policy level instead of hardcoding a single vendor forever
This fits CroohM's product posture: business users should not care which provider was used, only whether the task was completed safely and cheaply.
Decision: integrate, do not build.
Vibe Prospecting is built on Explorium's data platform. Explorium already exposes their full dataset via an MCP server (explorium-ai/mcp-explorium on GitHub), covering:
- 146M+ business entities, 767M+ employee profiles across 50+ data sources
- 4,000+ signals: funding events, IPOs, M&As, hiring signals, tech stack changes, website changes, intent data
- APIs: EventAPI (real-time triggers), FetchAPI (filter-based company/people search), EnrichAPI (multi-source enrichment)
- 10K records/min throughput, 97.8% firmographic accuracy
- Pricing: $200 for 5K credits (12-month expiry, no rollover)
CRoohM's Dispatch capability should consume Explorium's MCP server rather than building a proprietary data layer. The differentiation CRoohM adds is not the data itself — it is:
- The context layer: leads are already in the CRoohM inbox with conversation history — enrichment is applied to records the user already knows
- The execution layer: draft, approve, send WhatsApp/Gmail outreach — all inside CRoohM without switching apps
- The memory layer: Dispatch results feed the Brain's ContextSummary — future interactions are informed by the prospect research
This is the correct build vs buy decision for V1. Revisit building a proprietary data layer at Phase 4 if Explorium's coverage or pricing becomes limiting.
Integration approach:
- Dispatch agent exposes Explorium as a tool in its tool registry (
search_prospects,enrich_lead,get_intent_signals) - Tool calls route to Explorium MCP server via the tool runtime
- Credit consumption is tracked per workspace and attributed to AI usage billing
- Enriched data is written back to the Lead record in Firestore with
sourceType: "explorium"and timestamp
Cost model:
- Light enrichment (email + phone per lead): ~1–2 credits per lead
- Full enrichment (firmographics + signals + intent): ~5–10 credits per lead
- Prospect list search (20 results): ~10–20 credits
- At $200/5K credits: full enrichment runs ≈ $0.20/lead — well within SMB budget if leads are pre-qualified
Perplexity Computer (Feb 2026) orchestrates a routed multi-model environment in a sandboxed execution layer.
What this means for CroohM:
- The $200/month Perplexity Max user is CroohM's ideal enterprise customer profile — they pay for autonomous execution
- Perplexity's Sonar API (search-grounded LLM responses) is useful within CroohM's operator agent for research tasks: "Find this company's latest funding round" or "What is this contact's LinkedIn background?"
- CroohM can optionally route research-type tool calls to Perplexity Sonar rather than building its own web search — reduces build scope, leverages best-in-class search grounding
OpenClaw is an open-source, self-hosted personal AI assistant. The product went through three names: ClawdBot → Moltbot → OpenClaw. It connects to WhatsApp, Telegram, Slack, Discord, iMessage, Signal, Microsoft Teams, Matrix, Google Chat (10+ platforms simultaneously) and acts as one unified conversation layer across all of them.
Key capabilities (2026 verified):
- Proactive outreach: sends you messages first — reminders, alerts, morning briefings — without waiting to be prompted
- Persistent memory: remembers everything across sessions; never repeats context when picking up unfinished work
- Task execution: runs shell commands, controls the browser, reads/writes files, manages calendar, sends emails — all triggered by a text message in WhatsApp or Telegram
- Self-hosted: no SaaS subscription; you own the data and the compute
What this means for CRoohM:
- OpenClaw validates the core CRoohM thesis: multi-channel AI that acts beats single-channel AI that talks
- OpenClaw's biggest gaps are exactly CRoohM's strengths: no multi-tenant model, no workspace/team concept, no WhatsApp Business API (uses personal WhatsApp only), no CRM data layer
- OpenClaw's proactive surface pattern is the design reference for CRoohM's Brain proactive alerts ("3 leads haven't been followed up — want me to draft outreach?")
- OpenClaw's persistent memory model (facts, preferences, ongoing projects across sessions) maps directly to CRoohM's
ContextSummaryschema - The skill/plugin marketplace approach is worth studying for CRoohM's V3 Playbook template marketplace
Production-stable in 2026. Provider-agnostic (100+ LLMs). Key primitives: Agent, Runner, Handoff, Guardrail, Session.
Decision: Do not use OpenAI Agents SDK as CroohM's orchestration framework.
Rationale: LangGraph's graph model is better suited to CroohM's complex workflow needs (parallel fan-out, conditional approval branching, long-running pauses, checkpointing). OpenAI Agents SDK excels at linear agent flows. However, OpenAI Agents SDK's Guardrail primitive is a useful pattern to copy: pre-run and post-run validation functions that block unsafe inputs/outputs before they reach the agent or the user.
CroohM should implement a GuardrailLayer with the same semantics:
- Input guardrails: block prompt injection attempts, enforce workspace scope in user messages
- Output guardrails: detect PII in channel-bound messages, block unsupported instructions, enforce brand voice
Google's Agent Development Kit (Python + TypeScript, deployed to Cloud Run with adk deploy cloud_run). Supports Sequential, Parallel, Loop workflow agents. Integrates with Vertex AI Agent Engine. Uses MCP as a client standard.
Decision: Use as deployment reference, not as orchestration framework.
CroohM will use Cloud Run (ADK's target deployment platform) and will implement MCP (ADK's integration standard) but will use LangGraph instead of ADK's agent runtime. If CroohM adds Gemini as a V2 LLM provider, ADK becomes a natural orchestration option for Gemini-specific workflows.
{
id: string;
workspaceId: string;
tenantId: string;
type: "channel" | "operator" | "workflow";
status: "draft" | "live" | "paused" | "archived";
name: string;
persona: string; // system prompt persona section
brandVoice: string; // tone and style instructions
allowedSkills: string[]; // Skill IDs
approvalPolicy: ApprovalPolicy;
channelDeployments: string[]; // ChannelDeployment IDs
modelOverride?: string; // override default model for this agent
createdAt: Timestamp;
updatedAt: Timestamp;
}{
id: string; // runId
workspaceId: string;
tenantId: string;
agentId: string;
agentType: AgentType;
sessionLaneKey: string;
status: RunStatus;
triggeredBy: "user" | "workflow" | "webhook" | "scheduler";
triggeredByUserId?: string;
input: {
message: string;
objectType?: "lead" | "conversation" | "task";
objectId?: string;
channelId?: string;
};
output?: {
message?: string;
actions: ActionRecord[];
};
modelUsed: string;
tokenUsage: {
inputTokens: number;
outputTokens: number;
cachedTokens: number;
estimatedCostUsd: number;
};
startedAt: Timestamp;
completedAt?: Timestamp;
durationMs?: number;
langfuseTraceId?: string;
error?: string;
steps: AgentRunStep[]; // subcollection
}{
id: string;
runId: string;
stepType: "llm_call" | "tool_call" | "approval_gate" | "handoff" | "context_assembly";
toolName?: string;
toolInput?: Record<string, unknown>;
toolOutput?: Record<string, unknown>;
llmModel?: string;
promptTokens?: number;
completionTokens?: number;
cachedTokens?: number;
latencyMs: number;
status: "completed" | "failed" | "pending_approval" | "approved" | "rejected";
sequenceNumber: number;
createdAt: Timestamp;
}{
id: string;
runId: string;
stepId: string;
workspaceId: string;
requestedByUserId?: string;
approvalMode: ApprovalMode;
actionType: ToolRisk;
toolName: string;
payload: Record<string, unknown>; // what will be executed on approval
humanReadableSummary: string; // plain language description shown in mobile UI
status: "pending" | "approved" | "rejected" | "expired";
reviewedByUserId?: string;
reviewedAt?: Timestamp;
expiresAt: Timestamp; // auto-reject after TTL
createdAt: Timestamp;
}{
id: string;
workspaceId: string;
tenantId: string;
runId: string;
sessionLaneKey: string;
toolName: string;
targetObjectId: string;
idempotencyKey: string;
providerPayload: Record<string, unknown>;
providerName?: string;
providerMessageId?: string;
sendRequestId: string;
status: "prepared" | "committed" | "aborted" | "expired";
expiresAt: Timestamp;
committedAt?: Timestamp;
createdAt: Timestamp;
}{
id: string;
workspaceId: string;
tenantId: string;
type: "website" | "document" | "snippet" | "crm_object" | "template" | "playbook";
name: string;
syncStatus: "pending" | "syncing" | "synced" | "failed";
reviewStatus: "unreviewed" | "approved" | "rejected";
visibility: "workspace_internal" | "customer_safe" | "agent_only";
lastSyncedAt?: Timestamp;
chunkCount?: number;
rawFileGcsUri?: string;
createdAt: Timestamp;
updatedAt: Timestamp;
}{
id: string;
workspaceId: string;
sessionLaneKey: string; // the entity this summary belongs to
summaryShort: string; // 1–2 sentence summary for context assembly
summaryWorking: string; // working memory: current state, pending items
extractedFacts: string[]; // structured facts extracted from conversation
openLoops: string[]; // unresolved commitments or questions
customerIntent?: string; // for channel agents: what the customer wants
lastUpdatedAt: Timestamp;
turnCount: number;
}{
id: string;
workspaceId: string;
tenantId: string;
provider: "anthropic" | "openai" | "perplexity" | "google_vertex";
ownerScope: "platform" | "tenant" | "workspace";
mode: "platform_managed" | "byok";
allowedModels: string[];
allowedUseCases: string[];
healthStatus: "healthy" | "degraded" | "failed";
cooldownUntil?: Timestamp;
regionPolicy?: string;
billingLabel: string;
secretManagerRef: string; // path in Secret Manager — never the key itself
createdAt: Timestamp;
}{
id: string;
workspaceId?: string; // optional override; defaults to platform policy
name: string;
useCase: "planner" | "channel_reply" | "classification" | "summarization" | "retrieval_judge";
primaryRoute: string; // e.g. openrouter:minimax
fallbackRoutes: string[];
evalVersion: string;
status: "draft" | "active" | "deprecated";
updatedAt: Timestamp;
}{
id: string;
runId: string;
workspaceId: string;
source: "user" | "system" | "reviewer";
signalType: "thumbs_up" | "thumbs_down" | "approved" | "rejected" | "edited_before_send" | "handoff" | "task_completed" | "task_failed";
label?: string; // failure bucket label
notes?: string;
createdByUserId?: string;
createdAt: Timestamp;
}priorityScore: number; // 0–100, AI-computed
nextAction: string;
nextActionAt: Timestamp;
qualificationState: "unqualified" | "mql" | "sql" | "opportunity" | "lost" | "won";
lastAgentTouchAt?: Timestamp;
ownerTeamId?: string;agentDeploymentId?: string;
handoffState: "agent" | "human" | "closed";
lastAgentRunId?: string;
summaryShort?: string;
summaryWorking?: string;
openLoops?: string[];
customerIntent?: string;
resolutionState: "open" | "resolved" | "escalated";generatedByAgent: boolean;
approvalState?: "pending" | "approved" | "rejected" | "auto_sent";
runId?: string;
channelFormattingMode?: "whatsapp" | "email" | "chat";type AgentType = "channel" | "operator" | "workflow";
type RunStatus = "queued" | "planning" | "running" | "waiting_approval" | "completed" | "failed" | "cancelled";
type ToolRisk = "read" | "draft" | "write" | "external_send" | "admin" | "connector_admin";
type ApprovalMode = "none" | "send_only" | "external_only" | "all_mutations" | "policy_driven";
type CredentialMode = "platform_managed" | "byok";
type KnowledgeSourceType = "website" | "snippet" | "document" | "crm_object" | "conversation" | "template" | "playbook";
type HandoffState = "agent" | "human" | "closed";
type ResolutionState = "open" | "resolved" | "escalated";POST /v1/workspaces/{workspaceId}/agent/run
Body: { message: string, contextObjectType?, contextObjectId?, channelId? }
Response: { runId, status, streamUrl }
→ Creates an agent run, returns SSE stream URL for real-time token streaming
GET /v1/workspaces/{workspaceId}/agent/runs/{runId}
Response: AgentRun with steps subcollection
GET /v1/workspaces/{workspaceId}/agent/runs
Query: status?, agentType?, limit?, startAfter?
Response: AgentRun[]
POST /v1/workspaces/{workspaceId}/approvals/{approvalId}/approve
Body: { reviewNotes?: string }
Response: { status: "approved", resumeRunId }
POST /v1/workspaces/{workspaceId}/approvals/{approvalId}/reject
Body: { reason: string }
Response: { status: "rejected" }
GET /v1/workspaces/{workspaceId}/approvals
Query: status=pending
Response: ApprovalRequest[]
POST /v1/workspaces/{workspaceId}/agent/runs/{runId}/feedback
Body: { signalType, label?, notes? }
Response: RunFeedback
POST /v1/workspaces/{workspaceId}/agents
GET /v1/workspaces/{workspaceId}/agents
PATCH /v1/workspaces/{workspaceId}/agents/{agentId}
POST /v1/workspaces/{workspaceId}/agents/{agentId}/deploy → sets status=live
POST /v1/workspaces/{workspaceId}/agents/{agentId}/pause → sets status=paused
POST /v1/workspaces/{workspaceId}/knowledge/sources
Body: { type, name, url? } + multipart file upload
Response: KnowledgeSource (syncStatus=pending, triggers async indexing)
GET /v1/workspaces/{workspaceId}/knowledge/sources
GET /v1/workspaces/{workspaceId}/knowledge/sources/{sourceId}
DELETE /v1/workspaces/{workspaceId}/knowledge/sources/{sourceId}
POST /v1/workspaces/{workspaceId}/knowledge/search
Body: { query, topK?, visibilityFilter? }
Response: KnowledgeChunk[]
Agent responses stream as Server-Sent Events:
event: token
data: {"token": "Sure", "runId": "run_abc123"}
event: tool_call_start
data: {"toolName": "read_lead", "runId": "run_abc123"}
event: tool_call_result
data: {"toolName": "read_lead", "result": {...}, "latencyMs": 45}
event: approval_required
data: {"approvalId": "apr_xyz", "summary": "Send WhatsApp to John Doe: 'Following up...'"}
event: run_complete
data: {"runId": "run_abc123", "status": "completed", "durationMs": 3420}
This section documents the evaluated options and the rationale behind CroohM's chosen transport model.
How it works:
- All control-plane calls (start run, approve, reject, configure) → standard REST/HTTP POST/GET
- Agent response streaming → Server-Sent Events (SSE) on a long-lived HTTP connection
- Push notifications for approvals → Firebase Cloud Messaging (FCM)
- Real-time Firestore listeners on mobile for approval state changes
sequenceDiagram
participant App as Mobile App
participant GW as API Gateway (Go)
participant OR as Orchestrator
participant FS as Firestore
App->>GW: POST /agent/run {message, contextObjectId}
GW->>OR: forward with injected {tenantId, workspaceId}
OR-->>App: 200 {runId, streamUrl}
App->>OR: GET /stream/{runId} (SSE connection)
loop Token streaming
OR-->>App: event: token {data: "Sure,"}
OR-->>App: event: tool_call_start {data: "read_lead"}
OR-->>App: event: tool_call_result {data: {...}}
end
OR-->>App: event: approval_required {approvalId, summary}
OR-->>App: event: run_complete {status: "waiting_approval"}
Note over App,FS: FCM push notification triggers app foreground
App->>FS: listen on ApprovalRequests/{approvalId}
App->>GW: POST /approvals/{approvalId}/approve
GW->>OR: resume run
OR-->>App: event: run_complete {status: "completed"}
Why SSE and not polling:
- SSE is unidirectional server-to-client — exactly what token streaming requires
- SSE works over HTTP/1.1 — no infrastructure changes, CDN-compatible, Cloud Run native
- SSE reconnects automatically on disconnect — React Native handles this with the
EventSourcepolyfill - Firestore listeners handle the approval state change event — no polling needed
How it works:
- Bidirectional streaming via
.protoservice definitions - Client and server exchange structured protobuf messages
- Single persistent connection for the full run lifecycle
Evaluated against:
| Criterion | gRPC | REST + SSE |
|---|---|---|
| Mobile client support | Needs grpc-web proxy layer + React Native adapter |
Native fetch + EventSource — no adapter |
| Cloud Run + load balancer | Requires HTTP/2 end-to-end — extra LB config | HTTP/1.1 SSE — works out of the box |
| Streaming | Bidirectional — more power than needed | Server-to-client only — correct for this use case |
| Schema enforcement | Protobuf — strict and auto-generated | OpenAPI + JSONSchema — sufficient |
| Debug toolability | Requires specialized tooling (grpcurl, BloomRPC) |
curl, browser DevTools, Postman — zero setup |
| Team familiarity | Requires .proto authoring learning curve |
REST is universal |
Decision: gRPC is the right choice when you have high-throughput bidirectional streaming between internal services (e.g. ML inference pipelines). For CroohM's use case — a mobile client receiving token streams and occasional control messages — REST + SSE is simpler to operate and debug with no meaningful capability gap.
Revisit gRPC for: internal service-to-service calls between croohm-orchestrator and croohm-connectors if inter-service throughput becomes a bottleneck. This is a backend-only change with no client-side impact.
How it works:
- Full-duplex TCP socket connection
- Client and server exchange arbitrary messages in both directions
Evaluated against:
| Criterion | WebSocket | REST + SSE |
|---|---|---|
| Connection management | Stateful session — hard to route across Cloud Run replicas without sticky sessions | Stateless — any replica handles any SSE request |
| Mobile reconnect behavior | Must implement reconnect logic manually | EventSource auto-reconnects with Last-Event-ID |
| Cloud Run scaling | Sticky sessions or external connection registry needed | No special config — works natively |
| Use case fit | Required when client sends many messages on the same connection | Client only sends on control endpoints; server streams output |
| Operational complexity | Higher — need to handle heartbeats, ping/pong, connection registry | Lower — HTTP standard behavior |
Decision: WebSocket is the right choice for web chat widget (bidirectional, low-latency chat). It is not the right choice for the agent run streaming channel, where the server does the talking and the client only sends control signals on separate REST endpoints.
WebSocket used for: croohm-connectors web chat widget only — where the browser and CroohM need true bidirectional chat.
graph TD
A{What is being<br/>transported?} -->|Agent run streaming<br/>token-by-token output| B[REST + SSE<br/>GET /stream/runId]
A -->|Control commands<br/>run · approve · reject · configure| C[REST HTTP<br/>POST / GET / PATCH]
A -->|Approval push<br/>notify mobile even when app is closed| D[FCM Push<br/>via Firebase Cloud Messaging]
A -->|Real-time approval<br/>state change while app is open| E[Firestore<br/>onSnapshot listener]
A -->|Web chat widget<br/>browser ↔ agent, bidirectional| F[WebSocket<br/>in croohm-connectors]
A -->|Internal service calls<br/>orchestrator → connectors| G[REST HTTP<br/>V1 — revisit gRPC if bottleneck]
style B fill:#bbf7d0,stroke:#16a34a
style C fill:#bbf7d0,stroke:#16a34a
style D fill:#bbf7d0,stroke:#16a34a
style E fill:#bbf7d0,stroke:#16a34a
style F fill:#dbeafe,stroke:#2563eb
style G fill:#bbf7d0,stroke:#16a34a
stateDiagram-v2
[*] --> Idle
Idle --> Connecting: User sends instruction
Connecting --> Streaming: POST /agent/run → 200 OK + open SSE
Streaming --> StreamingTools: event: tool_call_start
StreamingTools --> Streaming: event: tool_call_result
Streaming --> WaitingApproval: event: approval_required
WaitingApproval --> WaitingApproval: Firestore listener active
WaitingApproval --> Resuming: User taps Approve
WaitingApproval --> Idle: User taps Reject
Resuming --> Streaming: SSE resumes
Streaming --> Idle: event: run_complete
Streaming --> Idle: SSE disconnects → auto-reconnect via Last-Event-ID
# Agent starts
event: run_started
data: {"runId": "run_abc123", "agentType": "operator"}
# Token stream (one event per token chunk)
event: token
data: {"token": "Sure,", "runId": "run_abc123"}
# Tool called
event: tool_call_start
data: {"toolName": "read_lead", "runId": "run_abc123", "stepId": "step_001"}
# Tool returned
event: tool_call_result
data: {"toolName": "read_lead", "latencyMs": 45, "status": "ok"}
# Approval needed — client shows approval UI
event: approval_required
data: {"approvalId": "apr_xyz789", "summary": "Send WhatsApp to John Doe: 'Following up on...'", "expiresAt": "2026-03-18T12:15:00Z"}
# Run paused for approval — SSE stream ends here
event: run_paused
data: {"runId": "run_abc123", "status": "waiting_approval"}
# After user approves — new SSE stream opened on resume
event: run_resumed
data: {"runId": "run_abc123", "approvalId": "apr_xyz789"}
# Final completion
event: run_complete
data: {"runId": "run_abc123", "status": "completed", "durationMs": 3420, "actionsCommitted": 3}
# Error
event: run_error
data: {"runId": "run_abc123", "error": "Lead not found", "retryable": false}
Reconnect strategy:
- SSE client sends
Last-Event-IDheader on reconnect - Orchestrator replays missed events from the last acknowledged event ID using LangGraph checkpoint state
- Max reconnect window: 5 minutes from run start; after that, client fetches run state via
GET /agent/runs/{runId}instead
| Threat Actor | Motivation | Example Attack |
|---|---|---|
| Malicious tenant user | Access another workspace's data | Inject workspace ID in request body |
| Adversarial customer | Manipulate agent behavior | Prompt injection via customer message |
| Compromised workspace user | Exfiltrate data via agent | "List all leads and email them to me@external.com" |
| LLM supply chain | Model behavior exploit | Jailbreak via crafted knowledge source content |
Tenant Isolation:
tenantIdandworkspaceIdare extracted from JWT and injected by the API gateway — never trusted from request body- Every Firestore query is parameterized with
workspaceId— no exceptions; enforced via middleware - Every vector retrieval query includes mandatory
tenantId+workspaceIdfilters — hardcoded in retrieval service, not configurable by callers - Firestore security rules provide defense-in-depth isolation
Prompt Injection Defense:
- User messages and customer-provided content are tagged with clear delimiters in the system prompt:
<user_message>,<customer_message>— never mixed into trusted instructions - Input guardrail runs before every LLM call: checks for known injection patterns, workspace override attempts
- Knowledge source content is indexed from controlled ingestion pipeline — not passed raw into prompts
Tool Execution Controls:
- Tool allowlist is workspace-configured and injected by infrastructure before the model sees it — user cannot expand the allowlist at runtime
- High-risk tools (
external_send,admin) require explicit workspace policy enablement + approval gate - Tool call payloads are validated against JSONSchema before execution
Credential Security:
- LLM API keys and OAuth tokens stored only in Secret Manager — never in Firestore documents
- Firestore stores only the Secret Manager reference path
- KMS-backed envelope encryption for any credential metadata stored in Firestore
- BYOK keys isolated to the workspace's credential profile — no cross-workspace key reuse
Audit Log:
- Every agent run, tool call, approval decision, and handoff is written to a structured audit log in Cloud Logging
- Audit log is append-only — no delete API
- Run traces are archived to Cloud Storage as immutable objects after 30 days
PII Controls:
- Langfuse traces have PII masking for known patterns (phone numbers, email addresses) before trace is written
- Provider retention and logging policy must be reviewed per active route and documented in
CredentialProfile; CroohM must not assume one provider's retention defaults apply to all routes
- GDPR: right-to-erasure implementation — delete lead data cascades to vector chunks and context summaries
- WhatsApp Business API Terms: no unsolicited messages outside 24h window; approved template fallback
- SOC 2 Type II: audit log completeness, access control review, encryption at rest/transit
- Data residency: Firestore region-pinning, vector region tagging for EU workspaces
| Artifact | System of Record | Default Retention | Delete Behavior |
|---|---|---|---|
| Messages / conversations | Firestore | product retention policy | cascade delete on lead/workspace deletion where legally allowed |
| Agent runs | Firestore + archive | 30 days hot, then archived | scrub PII references on delete request; retain minimal audit envelope if required |
| Agent run steps | Firestore + archive | 30 days hot, then archived | deleted with parent run archive policy |
| Approval requests | Firestore | 90 days | preserved until audit minimum, then purged |
| Prepared actions | Firestore | 30 days after terminal state | deleted after reconciliation window |
| Context summaries | Firestore | rolling latest + 30-day history | hard-delete with source conversation/lead deletion |
| Knowledge chunks / embeddings | vector store + metadata | until source deleted or resynced | delete by sourceId, workspaceId, tenantId |
| Uploaded knowledge artifacts | Cloud Storage | until source deleted or retention TTL | hard-delete plus tombstone record |
| Langfuse traces | self-hosted Langfuse | 30 days hot by default | redacted or deleted according to workspace policy |
| Audit archives | Cloud Storage | compliance-driven | immutable until retention expiry |
| Parameter | Estimate |
|---|---|
| Active workspaces | 500 at launch, 2,000 by month 12 |
| Agent runs per workspace per day | 15 average (operator: 10, workflow: 4, channel: 1) |
| Tokens per operator agent run | ~4,000 input, ~800 output (MiniMax-class route) |
| Tokens per channel agent turn | ~2,000 input, ~300 output (MiniMax-class route) |
| Tokens per workflow batch run | ~80,000 input, ~15,000 output (mixed routed batch, 20 leads) |
| Knowledge chunks per workspace | ~1,000 average, 300-dim embeddings |
| Concurrent active runs (peak) | ~80 (4% of workspaces at peak hour) |
| Workload | Runs/month | Tokens/run | Route Class | Cost/run | Total |
|---|---|---|---|---|---|
| Operator agent | 5,000 | 5K in / 1K out | MiniMax primary | route-dependent | baseline target only |
| Workflow batch | 2,000 | 80K in / 15K out | MiniMax + low-cost batch route | route-dependent | baseline target only |
| Channel agent | 500 | 2K in / 300 out | MiniMax primary | route-dependent | baseline target only |
| Classification | 7,500 | 500 in / 100 out | OSS/free route | route-dependent | baseline target only |
| Total LLM cost | budget target: $8–15/workspace/month |
Planning note:
- exact route costs must be recomputed from live OpenRouter pricing and actual chosen models at rollout time
- RFC budgeting should optimize for outcome cost per successful task, not raw token price
- launch target remains: stay within a blended AI compute budget of $8–15/workspace/month
croohm-orchestrator (Cloud Run):
- 1 vCPU, 2GB RAM per instance
- Average run duration: 3.5 seconds (mostly waiting on model/provider API)
- At 80 concurrent runs: 80 instances peak
- Cloud Run auto-scales to 0 at off-peak → significant cost reduction vs always-on
Vector backend:
- 500 workspaces × 1,000 chunks = 500,000 vectors at launch
- 2,000 workspaces × 2,000 chunks = 4,000,000 vectors at month 12
- managed shared vector backend should remain well below a dedicated-per-workspace design at month 12 scale
Redis (Memorystore):
- Session lane locks: ~80 concurrent (tiny)
- Rate limit counters: ~2,000 workspaces × 10 keys = 20,000 keys (tiny)
- Context cache: ~500 active sessions × 4KB = 2MB (tiny)
- Smallest Memorystore instance (1GB): more than sufficient
Firestore:
- Agent runs: 500 workspaces × 450 runs/month = 225,000 documents/month
- At 5KB/document: 1.1GB/month write — well within Firestore free tier for writes
- Run steps: 10 steps/run × 225,000 runs = 2.25M step documents/month — ~$0.45/month at $0.18/100K writes
| Component | Monthly Cost |
|---|---|
| LLM API (OpenRouter routed mix) | workload-dependent |
| Embedding API | provider-dependent |
| Cloud Run (all services) | ~$800 |
| Vector backend | provider-dependent |
| Firestore | ~$150 |
| Memorystore (Redis) | ~$100 |
| Cloud Storage | ~$50 |
| Pub/Sub + Cloud Tasks | ~$50 |
| Langfuse (Cloud Run + Cloud SQL) | ~$150 |
| Misc (Secret Manager, KMS, Logging) | ~$100 |
| Total | to be re-estimated from selected routes before implementation freeze |
Commercial model recommendation:
- include a small AI allowance in higher plans for activation
- add usage-based AI credits for heavier automation and bulk runs
- enterprise BYOK customers can pay platform fee + lower AI margin, or pure platform fee when CroohM does not bear model cost
CroohM should not charge only on tokens. Users buy outcomes.
Recommended commercial structure:
- Plan allowance: every qualifying workspace plan includes a monthly AI allowance for light operator use
- AI credits: bulk automations, high-volume channel sends, and heavy workflow runs consume credits
- Premium automation add-on: unlocks scheduled workflows, bulk runs, and advanced approvals
- BYOK enterprise mode: workspace supplies model credentials; CroohM charges platform fee for orchestration, observability, approvals, connectors, and storage
Metering dimensions:
- operator tasks completed
- channel agent sessions handled
- bulk workflow runs
- external sends committed
- premium research actions
Why this model:
- token billing is too abstract for SMB customers
- credits map better to user value and control spend better
- BYOK customers still pay for CroohM's orchestration layer, not just raw model access
| SLI | SLO | Window | Alert Threshold |
|---|---|---|---|
| Operator agent P95 response latency | < 4,000ms | 30-day rolling | Alert if > 6,000ms for 5 min |
| Channel agent P95 response latency | < 2,000ms | 30-day rolling | Alert if > 3,000ms for 2 min |
| Agent run success rate | ≥ 98.5% | 7-day rolling | Alert if < 97% for 15 min |
| Approval gate delivery latency (FCM → mobile) | < 3,000ms | 30-day rolling | Alert if > 5,000ms |
| Knowledge retrieval P95 latency | < 300ms | 30-day rolling | Alert if > 500ms |
| API gateway availability | ≥ 99.9% | 30-day rolling | Alert if < 99.5% for 5 min |
Error budget: 1 - SLO. For 99.9% availability: 43.8 minutes/month error budget. Burn rate alerts at 2x and 5x burn rate.
Business metrics (emitted by orchestrator):
croohm_agent_run_total{agent_type, status, workspace_id}
croohm_agent_run_duration_ms{agent_type, model, p50/p95/p99}
croohm_token_usage_total{model, type="input|output|cached", workspace_id}
croohm_tool_call_total{tool_name, status}
croohm_approval_request_total{status, action_type}
croohm_approval_resolution_time_ms{status}
croohm_handoff_total{from, to}
croohm_run_feedback_total{signal_type, label}
croohm_route_policy_success_rate{route_policy, use_case}
Infrastructure metrics (Cloud Monitoring):
run.googleapis.com/request_count{service, status}
run.googleapis.com/request_latencies{service, p95}
run.googleapis.com/container/instance_count{service}
firestore.googleapis.com/document/read_count
redis.googleapis.com/stats/memory/usage_ratio
- Token usage per workspace per day (cost breakdown)
- P50/P95 latency per model per agent type
- Cache hit rate per workspace
- Tool call success/failure rate per tool
- LLM-as-judge eval scores per agent type (task completion, faithfulness, hallucination rate)
- Prompt version comparison (before/after prompt change)
Run nightly via Cloud Scheduler → Cloud Run Job:
| Suite | # Test Cases | Eval Method | Pass Threshold |
|---|---|---|---|
| Tenant isolation | 50 | Verify cross-workspace retrieval returns 0 results | 100% |
| Approval gate enforcement | 30 | Verify external_send without approval = blocked | 100% |
| Operator agent task completion | 100 | LLM-as-judge on pinned eval route | ≥ 85% |
| Channel agent faithfulness | 80 | RAGAS faithfulness score | ≥ 0.80 |
| Session lane concurrency | 20 | Simulate concurrent runs on same leadId | 100% no duplicates |
| Knowledge retrieval recall | 60 | Context recall score (RAGAS) | ≥ 0.75 |
| Prompt injection resistance | 40 | Adversarial inputs, verify refusal | ≥ 95% |
| BYOK isolation | 20 | Verify workspace A's key not used for workspace B | 100% |
| Route regression | 40 | Compare new route policy against previous production baseline | no critical regression |
Any suite falling below threshold triggers a Slack alert and blocks the next release.
Goal: Production-ready operator agent with read + draft tools and full run tracing.
Deliverables:
-
WorkspaceAgent,AgentRun,AgentRunStep,ApprovalRequestFirestore schemas -
croohm-orchestratorservice (TypeScript + LangGraph.js) — deploy to Cloud Run - OpenRouter integration with primary MiniMax route and route policy registry
- Prompt caching on workspace config + platform policy prefix
- Context Engine v1: CRM object fetch + workspace config assembly
- Tool Runtime v1:
read_leads,get_lead,search_leads,get_pipeline_summary(all read-only) - Tool Runtime v2:
draft_whatsapp_message,draft_gmail_reply(draft-only, no sends) - Session lane manager (Redis distributed locks)
-
PreparedAction+ idempotent commit path - Run trace emission to Langfuse
- Feedback capture (
RunFeedback) + failure bucket labeling - Approval gate infrastructure (Firestore + FCM push notification to mobile)
- Approval UI in mobile app (list of pending approvals + approve/reject)
- API endpoints:
/agent/run,/agent/runs/{runId},/approvals/{id}/approve,/approvals/{id}/reject - Phase 1 eval suite: tenant isolation + approval gate enforcement + operator task completion
Milestone gates:
- M1.1 (Week 4): Operator can ask "Show me all hot leads with no reply in 48h" and get a correct list — with full run trace in Langfuse
- M1.2 (Week 6): Operator can ask "Draft follow-up WhatsApp messages for all hot leads" — drafts returned, no sends
- M1.3 (Week 8): Approval flow end-to-end: mobile push notification → review draft → approve/reject → run continues or aborts
Acceptance criteria for Phase 1:
- Tenant isolation eval: 100% pass
- Approval gate enforcement eval: 100% pass
- Operator task completion rate: ≥ 75% on golden dataset
- P95 response latency: < 5 seconds
- Zero production incidents in 2-week soak
Goal: Agent can actually send messages with approval gates.
Deliverables:
-
update_lead_statuswrite tool (with approval gate) -
send_whatsapp_messageexternal send tool (requiressend_onlyapproval) - WhatsApp template sync (approved templates for 24h window fallback)
- 24-hour window enforcement logic in connector
- Gmail OAuth per workspace, mailbox watch via Pub/Sub
- Gmail thread → Conversation mapping
-
draft_gmail_reply,send_gmailtools (send requires approval) -
send_whatsapp_templatetool for out-of-window contacts - Bulk operator run: Planner → N parallel Executor sub-agents for "follow up all hot leads"
- Phase 2 eval suite: WhatsApp send + Gmail send + 24h window logic + no-double-send concurrency
Milestone gates:
- M2.1 (Week 12): Agent sends WhatsApp message to a real lead (with approval) — zero duplicates under concurrent test
- M2.2 (Week 14): Agent replies to Gmail thread (with approval) — thread mapping correct
- M2.3 (Week 16): Bulk follow-up run: 20 leads, 20 drafts, batch approval UI, 20 sends — all correct
Goal: RAG-powered channel agent on web chat.
Deliverables:
- Knowledge ingestion pipeline (Python worker): document chunking, embedding, vector write
- Shared vector namespace model with strict tenant/workspace filtering
-
search_knowledgetool — retrieval with workspace filter - KnowledgeSource management APIs
- Knowledge review workflow (source must be
approvedbefore being used in customer-facing agent) - Knowledge source state model:
synced≠approved≠live on channel - Web chat channel deployment (WebSocket/SSE widget)
- Channel agent persona and behavior configuration
- Agent escalation + human handoff (set
handoffState = "human"on lead/conversation) - Auto-close logic (inactivity timeout)
- Context summarizer worker (sliding-window summary for long conversations)
- CroohM MCP server (read + draft tools exposed via MCP)
- Phase 3 eval suite: knowledge retrieval recall/faithfulness + handoff quality + unsupported question refusal
Milestone gates:
- M3.1 (Week 20): Web chat agent answers 10 test questions from workspace knowledge base — faithfulness ≥ 0.80
- M3.2 (Week 22): Agent correctly escalates when confidence is low — handoff state transitions correctly
- M3.3 (Week 24): CroohM MCP server operational — an MCP-compatible client can run
list_leadsanddraft_whatsapp_message
Goal: Event-driven and scheduled automations; WhatsApp channel agent.
Deliverables:
-
WorkflowDefinitionschema + workflow authoring API - Product-defined workflow templates (V1: no-code authoring)
- "Follow up leads with no reply after N days"
- "Daily pipeline digest for workspace owner"
- "Qualify new inbound leads from WhatsApp"
- "Route hot leads to owner and notify by WhatsApp"
- Workflow trigger: event-based (Pub/Sub) + scheduled (Cloud Scheduler)
- WhatsApp channel agent deployment (workspace phone number → agent)
- WhatsApp inbound → conversation → agent response → send (with approval or auto, per policy)
- Sub-agent bounded orchestration for workflow execution
- Role-based agent packs (owner, sales rep, CSM — different tool allowlists)
- Phase 4 eval suite: workflow execution correctness + WhatsApp agent quality
Goal: Enterprise readiness, BYOK, provider failover, advanced evals.
Deliverables:
- BYOK credential profile management (workspace provides OpenRouter, OpenAI, or Vertex credentials as allowed)
- Credential health monitoring + cooldown state
- Provider failover: if primary credential is degraded, failover to platform credential (if workspace permits) or queue run for retry
- Enterprise vector isolation mode (dedicated index namespace or self-hosted deployment)
- Per-tenant KMS envelope encryption
- Advanced eval loop: automatic regression detection, model upgrade A/B testing
- Langfuse-based eval experiment comparison (before vs after model change)
- Perplexity Sonar integration (research tool within operator agent)
- Dark launch (Week 7–8): Deploy orchestrator to production but disable all write and send tools. Run operator agent in read+draft mode with internal team workspaces only.
- Dogfood (Week 8): CroohM team's own workspace uses the agent daily for pipeline review and draft generation. Collect run traces, measure latency, identify prompt quality gaps.
- Alpha (Week 9): 5 friendly customer workspaces enabled. 2-week soak. Monitor: task completion rate, latency, approval request volume, error rate.
- Beta (Week 11): 50 workspaces. Feature flag rollout via workspace plan tier.
- GA (Week 13): All workspaces on AI-enabled plan tiers.
All agent features controlled by WorkspaceFeatureFlags:
ai_operator_agent: boolean
ai_channel_agent_webchat: boolean
ai_channel_agent_whatsapp: boolean
ai_gmail_integration: boolean
ai_workflow_engine: boolean
ai_byok: boolean
ai_bulk_runs: boolean
Flags set per workspace by CroohM admin — enables progressive rollout and instant kill switch per feature per workspace.
- Orchestrator: Cloud Run traffic split — 100% old revision in < 30 seconds
- Workflow engine: Cloud Scheduler job pause — stops all scheduled runs immediately
- Agent kill switch: set all
WorkspaceAgent.status = "paused"via admin API — stops all agent runs within seconds - Langfuse traces are the rollback evidence — all decisions are auditable
Each user gets their own private AI brain with personal memory and context.
Rejected because:
- Breaks shared inbox — two reps get different contexts on the same customer
- Duplicates CRM and conversation context per user
- Complicates approval and handoff flows
- Channel agents must be workspace-deterministic — a WhatsApp number can't have 5 different AI personas
One physical vector deployment per customer workspace.
Rejected because:
- At 500 workspaces: 500 clusters to provision, monitor, and scale
- Cost is prohibitive — minimum instance per cluster × 500
- Operational complexity scales linearly with customer count
- shared vector infrastructure with strict host-side filtering provides sufficient V1 isolation without per-cluster overhead
- Revisit at 5,000+ workspaces or for enterprise compliance requirements
Single-language stack for simplicity.
Rejected because:
- GIL limits concurrency in orchestration layer at scale
- Cold start times and memory footprint make Python uncompetitive for the API gateway
- Cost at 100+ Cloud Run instances is meaningfully higher than Go for the gateway role
- Python is still the right choice for the embedding and eval workers where ML ecosystem matters
Temporal provides durable execution with its own server cluster.
Deferred because:
- Adds a Temporal cluster to operate (or Temporal Cloud cost: ~$200-500/month)
- LangGraph's Firestore-backed checkpointing is sufficient for V1 workflow durations (< 30 minutes)
- Temporal is the right upgrade path when workflows regularly exceed 30 minutes or need cross-datacenter durability
- Decision: re-evaluate at Phase 4 when workflow engine is built
Bind the RFC to one vendor-specific orchestration path from day 1.
Rejected because:
- CroohM wants to start with MiniMax and open-model experimentation, which requires routing flexibility
- pricing, latency, and quality are moving too quickly to lock the platform to one provider assumption
- provider abstraction is part of the commercial model because BYOK and enterprise routing are product requirements
- OpenRouter-first keeps the runtime adaptable while preserving one operational API surface
Simplify UX by executing all tools automatically without approval.
Rejected because:
- Irreversible external sends (WhatsApp, Gmail) require human review — this is a hard business requirement
- Approval gates are the primary trust mechanism for AI adoption among SMB users
- Without approval gates, a single bad agent run can send hundreds of incorrect messages
- The right model: approval gates on
external_sendandwrite, not onreadanddraft
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Prompt injection via customer message | Medium | High | Input guardrails + delimiter-based separation in system prompt + adversarial eval suite |
| Cross-workspace retrieval leak | Low | Critical | Mandatory tenant/workspace retrieval filter + Firestore security rules + isolation eval suite (100% pass gate) |
| Duplicate WhatsApp sends (session lane failure) | Low | High | Redis distributed locks + idempotency keys on send tool + no-double-send eval suite |
| LLM cost overrun from runaway loops | Medium | Medium | Max tool call limit per run (configurable, default 20) + token budget per run + per-workspace monthly credit cap |
| Primary model route latency spikes blocking channel agent | Medium | High | P95 < 2s SLO + Langfuse alerting + route fallback policy + error message to customer with retry |
| WhatsApp business API violations | Low | Critical | 24h window enforcement in connector + approved template fallback + meta compliance audit |
| Knowledge base contamination (approved bad content) | Low | High | Review workflow before source goes live + faithfulness eval + human review gate for customer-safe sources |
| Over-building orchestration before core tools are reliable | High | Medium | Phase 1 acceptance criteria gates — must pass eval suite before Phase 2 begins |
| BYOK key misuse (workspace A's key used for workspace B) | Very Low | Critical | CredentialProfile scoped strictly to workspaceId + BYOK isolation eval suite (100% pass gate) |
| Approval fatigue leading to blind approvals | Medium | Medium | Batch approval UI (review multiple at once) + intelligent grouping + approval analytics dashboard |
-
Enterprise vector isolation: Should enterprise customers get a dedicated vector deployment or dedicated namespace isolation as a paid add-on from day 1 (Phase 5), or wait until demand is demonstrated by actual customer requests?
-
Web chat vs WhatsApp channel agent launch order: Web chat is technically simpler (no WhatsApp 24h window complexity). WhatsApp is higher business value for CroohM's customer base. Should Phase 3 prioritize web chat (faster to ship) or WhatsApp channel agent (higher customer impact)?
-
Approval UX: Single-approval (one tap per draft) vs batch-approval (approve all drafts in one swipe) as the default. Single is safer; batch is necessary for bulk workflows. What is the right default per operation type?
-
Workflow authoring scope for V1: Product-defined templates only (fastest to build), or a simple condition-based no-code editor (higher flexibility)? Templates are lower risk but create a support burden when customers want customization.
-
Perplexity Sonar integration scope: Integrate as an optional research tool in operator agent from Phase 2, or defer to Phase 5? The integration is 1–2 days of work given the provider abstraction, but adds an external API dependency and cost.
-
MCP server auth model: OAuth 2.0 scoped to workspace (correct but adds auth complexity) vs API key per workspace (simpler but less secure). What is the right initial model for the developer audience?
| Date | Decision | Rationale | Owner |
|---|---|---|---|
| 2026-03-18 | OpenRouter as provider control plane | Enables MiniMax-first routing, OSS experimentation, and BYOK/provider flexibility behind one API surface | Engineering Lead |
| 2026-03-18 | TypeScript for orchestration | LangGraph.js ecosystem and strong provider-routing SDK support; same language as web frontend | Engineering Lead |
| 2026-03-18 | Go for API gateway | Concurrency model, low memory footprint, fast cold start | Engineering Lead |
| 2026-03-18 | LangGraph.js over OpenAI Agents SDK | Graph model required for parallel fan-out, conditional branching, long-running approval pauses | Engineering Lead |
| 2026-03-18 | Shared vector infrastructure with strict host-side filtering | Lower operational cost while preserving required tenant/workspace isolation semantics | Engineering Lead |
| 2026-03-18 | Langfuse self-hosted | Open source, data residency on GCP, full LLM tracing + eval integration | Engineering Lead |
| 2026-03-18 | Temporal deferred to V2 | LangGraph Firestore checkpointing sufficient for V1 workflow durations | Engineering Lead |
| 2026-03-18 | Workspace-first operating model | Channels are workspace assets; team consistency requires workspace-level AI behavior | Product Lead |
| 2026-03-18 | Approval gates on external_send and write | Trust mechanism for AI adoption; irreversible actions require human review | Product Lead |
- OpenRouter Quickstart
- OpenRouter Provider Routing
- OpenRouter Tool Calling
- OpenRouter Prompt Caching
- OpenRouter Zero Data Retention
- OpenRouter Free Models Router
- MiniMax API Overview
- MiniMax Compatible OpenAI API
- MiniMax Text Generation
- MiniMax Function Call Guide
- LangGraph — Agent Orchestration Framework
- OpenAI Agents SDK
- OpenAI Agents SDK TypeScript
- OpenAI Agent Evals Guide
- Google Agent Development Kit (ADK)
- Temporal — Durable Execution for AI Agents
- Model Context Protocol (MCP)
- Host AI agents on Cloud Run
- Cloud Run Overview
- Cloud Tasks
- Pub/Sub
- Langfuse Documentation
- Langfuse Experiments API Reference
- Vibe Prospecting — Intent-First AI Sales Intelligence
- Vibe Prospecting MCP Server (Explorium)
- Explorium — B2B Data Foundation for AI Agents
- Explorium MCP Server
- Explorium Developers — Quick Start
- ManyChat — WhatsApp Automation
- ManyChat Pricing 2026
- Intercom Fin — AI Agent for Customer Service
- Intercom Fin Pricing — $0.99/resolution
- item.app — The AI-Native CRM (YC-backed)
- OpenClaw — Open-Source Personal AI Assistant
- OpenClaw GitHub