InboxMate Architecture & Data Model
This is a context-loading skill. Read it, internalize the model, then proceed with whatever task you were doing.
The Five Systems
InboxMate spans five independent systems. Each owns its data and exposes its own API. No system directly reads another's database — they're connected through shared IDs.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Twenty CRM │ │ AgentHub DB │ │ Notif. Svc │
│ (GraphQL) │ │ (Supabase) │ │ (REST) │
│ │ │ │ │ │
│ Opportunities│────▶│ demo_pages │ │ email_drafts │
│ Companies │ ↑ │ agents │ │ email_queue │
│ Campaigns │ │ │ knowledge_* │ │ │
│ People/Notes │ │ │ │ │ │
└──────────────┘ │ └──────────────┘ └──────────────┘
│ │ │
│ serves demo │
│ page content │
│ ▼ │
│ ┌──────────────┐ │
│ │ Demo Pages │ │
│ │ (frontend) │ │
│ │demo.inboxmate│ │
│ │.psquared.dev │ │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
└──│ Ackee │◀────────────┘
│ (analytics) │ (tracks views)
└──────────────┘
The Key ID: How Objects Link Across Systems
The CRM Opportunity is the central record. Everything else hangs off it.
CRM Opportunity
├── .id → used as crm_opportunity_id in email_drafts
├── .demoId → points to demo_pages.id in AgentHub DB
├── .companyId → points to CRM Company
├── .campaignId → points to CRM Campaign
└── .demoStatus → state machine (see below)
AgentHub: demo_pages
├── .id → this IS the demoId stored in CRM
├── .agent_id → points to agents table
└── .crm_opportunity_id → backlink (often NULL, don't rely on it)
AgentHub: agents
├── .knowledge_bucket_ids → uuid[] pointing to knowledge_buckets
└── .published_at → non-null means live
AgentHub: knowledge_buckets → knowledge_bucket_items → knowledge_bucket_chunks
└── items have: source_type ('url' | 'knowledge_link' | 'text' | 'pdf')
└── items have: embedding_status ('pending' | 'processing' | 'completed' | 'failed')
Notification Service: email_drafts
├── .crm_opportunity_id → links back to CRM Opportunity
├── .crm_company_id → links to CRM Company
├── .draft_type → 'outreach' | 'followup'
├── .parent_draft_id → followup points to its outreach draft
└── .status → 'DRAFT' | 'QUEUED' | 'SENT' | 'FAILED'
Critical rule: The CRM opportunity's demoId field is the only reliable cross-system link between CRM and AgentHub. The demo_pages.crm_opportunity_id column exists but is often NULL. When resolving CRM → demo page, always query CRM for demoId, don't query Supabase by crm_opportunity_id.
Object States
CRM Opportunity.demoStatus (the pipeline state machine)
┌─────────────────┐
│ PENDING_REVIEW │ ← demo built, awaiting QA
└────────┬────────┘
pass │ │ fail
▼ ▼
┌────────────┐ ┌───────────┐
│ OK_TO_SEND │ │ NEEDS_FIX │ → fix → back to PENDING_REVIEW
└─────┬──────┘ └───────────┘
│ campaign + email sent
▼
┌──────────┐
│ SENT │ ← outreach email sent, outreachSentAt set
└─────┬────┘
│ follow-up sent
▼
┌─────────────────┐
│ FOLLOW_UP_SENT │
└─────────────────┘
Skip states (terminal): SKIP_FAULTY_WEBSITE, SKIP_IRRELEVANT, SKIPPED, DISQUALIFIED
CRM Opportunity.stage
| Stage | Meaning |
|---|---|
SCREENING |
Outreach phase (all demo pipeline work happens here) |
MEETING |
Prospect responded / call scheduled |
PROPOSAL |
Quoted or decision-maker demo |
CUSTOMER |
Paying customer |
Email Draft.status
DRAFT → (schedule) → QUEUED → (send job) → SENT
→ FAILED
Follow-up drafts are created alongside initial outreach (linked via parent_draft_id), with send_after_days controlling when they queue.
Campaign Structure
Campaigns group opportunities into sendable batches. A campaign has offerExpiresAt and offerText which are injected into demo pages dynamically (not stored on demo_pages directly — the API merges them at read time with a 5-min cache).
Data Flow: Demo Creation → Email Outreach
1. Lead Found → CRM
- Company + Person created in CRM
- No opportunity yet
2. Demo Built → AgentHub + CRM
- Agent created via MCP (
create_agent) - Knowledge bucket created, URLs scraped, items embedded
- Demo page created via MCP (
create_demo_page) → returnsdemo_pages.id - CRM Opportunity created at stage=SCREENING, demoStatus=PENDING_REVIEW
demoId= the demo page UUIDdemoUrl=https://demo.inboxmate.psquared.dev/?id=<demoId>
3. QA Review → CRM status change
/review-demosfetches PENDING_REVIEW opportunities- Checks agent, colors, knowledge, greeting
- Sets demoStatus to OK_TO_SEND or NEEDS_FIX
4. Campaign Planned → CRM
/plan-campaigncreates a CRM Campaign- Links selected opportunities via
campaignId - Sets
offerTextandofferExpiresAton the campaign
5. Email Drafts Created → Notification Service
/setup-email-draftsqueries OK_TO_SEND opportunities- Creates outreach draft + follow-up draft pair per opportunity
- Drafts stored with
crm_opportunity_idfor back-linking
6. Emails Scheduled & Sent → Notification Service + CRM
- Admin schedules drafts → status: QUEUED, enters
email_queue - Send job fires → status: SENT, CRM updated to demoStatus=SENT
- Follow-up auto-queues based on
send_after_days
7. Prospect Claims Demo → AgentHub
- Prospect clicks CTA on demo page
- Claim flow: clones agent + knowledge to new account
- Marks
demo_pages.claimed = true - Sends Telegram notification + welcome email
- Updates CRM stage/status
API Quick Reference
| System | Endpoint | Auth Token |
|---|---|---|
| CRM | POST https://crm.psquared.dev/graphql |
$PSQUARED_CRM_TOKEN |
| MCP | POST https://app.psquared.dev/api/mcp |
$NUXT_MCP_DEMO_TOKEN |
| Demo API | GET https://app.psquared.dev/api/demo/<id> |
None (public) |
| Notif. Svc | https://notifications.psquared.dev/drafts/* |
$EMAIL_DRAFT_ONLY_BEARER |
| Sanity Check | POST https://notifications.psquared.dev/drafts/sanity-check |
$EMAIL_DRAFT_ONLY_BEARER |
| Ackee | POST https://ackee.psquared.dev/api |
$ACKEE_TOKEN |
| Supabase | mcp__plugin_supabase_supabase__execute_sql |
project: fevtfywriufbqnvbgyrm |
Sanity Check
The notification service exposes a sanity check that validates demo agent health. Accepts three input modes:
// By opportunity IDs
{ "crm_opportunity_ids": ["uuid", ...] }
// By campaign
{ "campaign_id": "uuid" }
// By draft IDs
{ "draft_ids": ["uuid", ...] }
Returns per-item: healthy boolean + issues array + warnings array. Checks:
- Health failures (unhealthy): demo/agent not found, not published, empty buckets, text-type items, failed/processing embeddings, 404 content detected, >50% items under 50 tokens
- Warnings (non-blocking): missing source URLs, some low-token items, thin avg tokens, claimed demos
- Also checks: demo API reachability, email draft link correctness
Response includes demo_page_id for cross-system debugging, and avg_tokens for knowledge quality assessment.
Common Gotchas
CRM uses inline IDs, not parameterized variables. Twenty CRM's GraphQL does NOT support
$id: ID!parameters. Always inline UUIDs directly in the query string.Don't
source .env— values contain semicolons. UseReadtool orgrepto extract specific values.MCP token is
NUXT_MCP_DEMO_TOKEN(notINBOXMATE_DEMO_MCP_TOKEN). Some older skills use the wrong name.knowledge_link= URL-sourced. Inknowledge_bucket_items.source_type, items scraped from URLs are stored asknowledge_link, noturl. Both are valid URL-sourced content.Campaigns manage deadlines, not individual demos. Never set
offerExpiresAtorofferTextdirectly ondemo_pages— these come from the campaign and are merged dynamically at API read time.Follow-up emails: never mention "follow-up" in the subject. Body must be unique, not a generic template. Always include
variablesin PUT body when updating drafts.Never guess URLs for scraping. Always discover real pages from the homepage navigation first. Generic patterns (
/kontakt/,/ueber-uns/,/leistungen/) fail on 60%+ of sites.Tavily failures → WebFetch fallback.
scrape_and_build_knowledgeuses Tavily. Some sites block it (all URLs timeout). When this happens, use WebFetch to scrape pages manually and add viaadd_to_bucketwithsourceUrl. Never leave a bucket empty.Skip junk URLs:
/wp-json/,/xmlrpc.php,/feed/,/favicon.ico,.webp,.ico— these produce garbage knowledge items.Content limit is 50K characters per knowledge item. Tavily advanced mode can return large pages — they won't be truncated.
404 pages are auto-detected by
scrape_and_build_knowledge— pages with "nicht gefunden"/"not found"/"404" in the title are rejected and returned in thefailedarray. No need to check manually.
What Skill Do I Need?
Based on what you're trying to do, use:
| Goal | Skill |
|---|---|
| Need new leads | /find-leads [N] |
| Create demos for CRM leads | /inboxmate-batch-demo or /inboxmate-demo <company> |
| QA existing demos | /review-demos |
| Fix flagged demos | /fix-demos |
| Check if demos are healthy | /sanity-check |
| Group demos into a campaign | /plan-campaign |
| Create outreach emails | /setup-email-drafts [campaignId] |
| Edit email drafts | /refine-email-drafts |
| Check how outreach is doing | /check-outreach-status |
| Full funnel analytics | /check-demo-analytics |
| Upgrade demo knowledge | /refurbish-demos |
| Change pricing | /price-change |
| Deep debugging / raw queries | /analyse-inboxmate |
Pipeline Order
If starting from scratch, follow this sequence:
/find-leads → /inboxmate-batch-demo → /review-demos → /fix-demos (if needed)
→ /sanity-check → /plan-campaign → /setup-email-drafts → /refine-email-drafts (if needed)
→ 👤 send → /check-outreach-status → /check-demo-analytics