skills/setup-email-drafts/SKILL.md
Create email drafts for approved InboxMate demos. Verifies all demos are ready, pulls contacts from CRM, creates CRM tasks, and creates draft emails via the notification service. Run after /review-demos has processed all pending demos.
npx skillsauth add psquared-development/psquared-skills setup-email-draftsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
InboxMate is a white-label AI chatbot that businesses embed on their website. It answers customer questions 24/7 using the company's own knowledge base (products, pricing, FAQ). Visitors chat with it directly on the site. It handles lead qualification, appointment scheduling, and support — in the company's brand colors and language. Built by psquared, an Austrian AI company.
The demo we built for them is a live, working chatbot already configured with THEIR products and knowledge. This is the key differentiator — we're not pitching a generic tool, we're showing them something that already works for their business.
Before writing any email, read references/outreach-principles.md in this skill's directory. Key rules: lead with the demo (not a pitch), be specific to their business, keep it short (3-5 paragraphs), one CTA (the demo button). Every email must be unique — no templates, no AI patterns.
Announce:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Email Draft Pipeline started. Checking environment... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Read .env using the Read tool (do NOT source it — values may contain semicolons or special characters that break shell parsing). Extract the token values by reading the file content directly.
The .env file should contain:
PSQUARED_CRM_TOKEN — for querying opportunities and creating tasksEMAIL_DRAFT_ONLY_BEARER — for creating email drafts via the notification service. This token can read, create, and update drafts but cannot send, schedule, or delete them.If the .env file is missing either token, stop immediately and ask the user to provide them.
Once verified, announce:
Environment OK. Checking demo readiness...
IMPORTANT: When sending GraphQL queries to the CRM via curl, always use double-quoted -d strings with escaped inner quotes. Single-quoted strings cause intermittent parse failures with the Twenty CRM GraphQL endpoint when queries contain nested object fields like demoUrl { primaryLinkUrl }.
Do this:
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $CRM_TOKEN" \
-d "{\"query\":\"{ opportunities(first: 5) { edges { node { id name } } } }\"}"
NOT this (breaks with nested fields):
curl -s -X POST https://crm.psquared.dev/graphql \
-d '{"query":"{ opportunities(first: 5) { edges { node { id name demoUrl { primaryLinkUrl } } } } }"}'
All curl examples below use the safe double-quoted form.
Query CRM for ANY opportunities at SCREENING stage with demoStatus = PENDING_REVIEW:
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d "{\"query\":\"{ opportunities(filter: { stage: { eq: SCREENING }, demoStatus: { eq: PENDING_REVIEW } }, first: 5) { edges { node { id name } } } }\"}"
If any PENDING_REVIEW found: Stop and announce:
❌ [N] demos still pending review: - [Company A] - [Company B] Run /review-demos first before setting up email drafts.
If none found: Continue.
Query CRM for opportunities with demoStatus = OK_TO_SEND, including taskTargets in the response so we can filter out already-processed ones client-side:
IMPORTANT: The Twenty CRM does NOT support relation filters like taskTargets: { is: NULL } on OpportunityFilterInput. The taskTargets field is only available as a response field on the Opportunity type, not as a filter. You MUST fetch taskTargets in the response and filter client-side.
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d "{\"query\":\"{ opportunities(filter: { stage: { eq: SCREENING }, demoStatus: { eq: OK_TO_SEND } }, first: 150) { edges { node { id name taskTargets { edges { node { id } } } demoUrl { primaryLinkUrl } company { id name domainName { primaryLinkUrl } people(first: 5) { edges { node { id name { firstName lastName } emails { primaryEmail } } } } } } } } }\"}"
Client-side filtering: After receiving the response, skip any opportunity where taskTargets.edges is non-empty (length > 0). These have already been processed by a previous run. Add them to a "Skipped (already processed)" list in the report.
For each remaining opportunity, extract:
opportunityId, opportunityNamecompanyId, companyName, companyDomaindemoUrl (from demoUrl.primaryLinkUrl)company.people — pick the first person with an email. For multi-contact companies, prefer a named person (not "Office") if available.If company has no people or no email: Add to skip list with reason "No contact email found". Continue to next.
If no OK_TO_SEND opportunities (after filtering): Announce "No demos ready to send" and stop.
Announce:
Found [N] demos ready for outreach: 1. [Company Name] → [contact email] 2. [Company Name] → [contact email] ... Skipped (no contact): [list if any]
Template UUIDs (hardcoded — do NOT query the notification service API or Supabase MCP for these):
| Template | Locale | UUID |
|----------|--------|------|
| demo-outreach | de | b98926be-5977-40a6-9be6-ffe38989fc5a |
| demo-outreach | en | 47381011-a737-4157-a177-f7646bb4aee3 |
For each opportunity, determine the locale:
dedeenUse the matching UUID above.
THIS CHECK IS NOT OPTIONAL. Do NOT skip it. Do NOT proceed to 4b without running this check.
Note: CRM task deduplication is already handled in Step 2 by fetching taskTargets and filtering client-side. This step only checks the notification service.
Query notification service for existing drafts by opportunity ID:
curl -s -X GET "https://notifications.psquared.dev/drafts?crmOpportunityId=[opportunityId]&pageSize=1" \
-H "Authorization: Bearer $EMAIL_DRAFT_ONLY_BEARER"
If the response contains any drafts (array length > 0, regardless of status) → SKIP. Announce: SKIP: [Company Name] — draft already exists
Only proceed to 4b if no drafts found.
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d "{\"query\":\"mutation { createTask(data: { title: \\\"Send initial outreach for Demo [Company Name]\\\", status: TODO }) { id } }\"}"
Save the taskId from the response — it will be passed to the draft creation so we can reliably delete this exact task if the draft is deleted.
Then link to opportunity:
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d "{\"query\":\"mutation { createTaskTarget(data: { taskId: \\\"[taskId]\\\", opportunityId: \\\"[opportunityId]\\\" }) { id } }\"}"
The email template is a pure layout shell — you write ALL the text. The template only provides the InboxMate header, green CTA button, highlight box, signoff area, and p² footer. Everything else comes from your variables.
Writing style:
Determine tone (du vs Sie) for German emails — THIS IS CRITICAL, DO NOT SKIP:
For EACH company, before writing any text, check for prior communication:
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d "{\"query\":\"{ people(filter: { companyId: { eq: \\\"[companyId]\\\" } }, first: 5) { edges { node { emails { primaryEmail } } } } }\"}"
Then check if we have email threads with any of those email addresses in the agenthub DB:
curl -s -X POST https://crm.psquared.dev/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PSQUARED_CRM_TOKEN" \
-d "{\"query\":\"{ noteTargets(filter: { companyId: { eq: \\\"[companyId]\\\" } }, first: 20) { edges { node { note { title body } } } } }\"}"
Look at the note/email content for "du/dir/dein" vs "Sie/Ihnen/Ihr" patterns.
Rules:
Verify your choice: After writing all text for a company, re-read every sentence and check that du/Sie is consistent. A single "Ihre" in an otherwise du-Form email is a dealbreaker.
Template variables:
greeting — Personal. e.g. "Hallo [Vorname]," (du) or "Guten Tag Herr/Frau [Nachname]," (Sie)bodyParagraph1 — The hook. What you did and why it matters to THEM. One strong sentence.bodyParagraph2 — (optional) The personalized insight. Something specific about their website/business that shows you actually looked. This is what makes them keep reading.bodyParagraph3 — (optional) The nudge toward the demo. Keep it short.buttonText — CTA button label. "Demo ansehen" / "View Demo" / or something more specifichighlightTitle — (optional) Bold title for the green box. e.g. "Was der Bot für [Company] tun kann:" or "What this means for [Company]:"highlightText — (optional) One punchy line about what InboxMate does for THIS company specifically. Not generic features.closingText — (optional) Brief closing. "Bei Fragen einfach antworten." / "Just reply if you have questions."signoff — "Beste Grüße" / "Liebe Grüße" / "Best regards" — match the tonesenderName — "Martin"demoUrl — the demo playground URLcompanyName — used in email titleemailSubject — the rendered subject line (same as what you set on the draft)curl -s -X POST https://notifications.psquared.dev/drafts/create \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $EMAIL_DRAFT_ONLY_BEARER" \
-d '{
"templateId": "[template UUID from step 3]",
"locale": "[de|en]",
"subject": "[curiosity-driven subject line — see principle #8]",
"recipientEmail": "[contact email]",
"recipientName": "[contact first name]",
"variables": {
"companyName": "[Company Name]",
"demoUrl": "[demo playground URL]",
"greeting": "[personal greeting]",
"bodyParagraph1": "[the hook — what you did, why it matters]",
"bodyParagraph2": "[personalized insight about their business]",
"bodyParagraph3": "[nudge toward the demo]",
"buttonText": "[CTA button text]",
"highlightTitle": "[bold title for green box]",
"highlightText": "[what InboxMate does for THIS company — one punchy line]",
"closingText": "[brief closing]",
"signoff": "[matching tone signoff]",
"senderName": "Martin"
},
"crmCompanyId": "[company ID]",
"crmOpportunityId": "[opportunity ID]",
"crmCompanyName": "[Company Name]",
"crmTaskId": "[taskId from step 4b]"
}'
Announce after each:
Draft created: [Company Name] → [email]
Note: This skill does NOT update CRM fields. The notification service updates them automatically when emails are sent from the admin UI:
| On send | Field | Value |
|---------|-------|-------|
| Outreach email sent | demoStatus | SENT |
| Outreach email sent | outreachSentAt | current timestamp |
| Follow-up email sent | demoStatus | FOLLOW_UP_SENT |
| Follow-up email sent | followupSentAt | current timestamp |
Announce:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Email Draft Setup complete. Drafts created: [N] - [Company A] → [email protected] - [Company B] → [email protected] Skipped (draft already exists): [N] - [Company C] — task already linked Skipped (no contact email): [N] - [Company D] — no contact with email found CRM tasks created: [N] Next step: Review and send drafts at → notifications.psquared.dev/drafts ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
tools
Generate a polished psquared client offer as a multi-page PDF (title, project description, screenshots, Angebot/pricing, AGB). Walks the user through gathering inputs (or accepts a JSON config), renders branded HTML templates with Playwright in two passes (title page edge-to-edge + body pages with margins and pagination), then merges with pdf-lib.
development
Audit or fix SEO issues for a single website or page. Checks meta tags, structured data, technical SEO, content quality, i18n, and AI readiness using only WebFetch — no external APIs. Pass a URL and mode (audit or fix) as parameters.
development
Run SEO audit or fix across all psquared websites autonomously. Dispatches parallel agents for psquared.dev, inboxmate.psquared.dev, ki-linz.at, and agenthub.psquared.dev, then presents a combined report. Pass mode (audit or fix) as parameter.
testing
Verify all InboxMate demo agents are properly configured — checks greetings, quick questions, knowledge, colors, and CRM linkage. Run after batch demo creation or before sending outreach.