@clawhub-alirezarezvani-9164a8924b
Env & Secrets Manager
--- name: "env-secrets-manager" description: "Env & Secrets Manager" --- # Env & Secrets Manager **Tier:** POWERFUL **Category:** Engineering **Domain:** Security / DevOps / Configuration Management --- ## Overview Complete environment and secrets management workflow: .env file lifecycle across dev/staging/prod, .env.example auto-generation, required-var validation, secret leak detection in git history, and credential rotation playbook. Integrates with HashiCorp Vault, AWS SSM, 1Password CLI, and Doppler. --- ## Core Capabilities - **.env lifecycle** — create, validate, sync across environments - **.env.example generation** — strip values, preserve keys and comments - **Validation script** — fail-fast on missing required vars at startup - **Secret leak detection** — regex scan of git history and working tree - **Rotation workflow** — detect → scope → rotate → deploy → verify - **Secret manager integrations** — Vault KV v2, AWS SSM, 1Password, Doppler --- ## When to Use - Setting up a new project — scaffold .env.example and validation - Before every commit — scan for accidentally staged secrets - Post-incident response — leaked credential rotation procedure - Onboarding new developers — they need all vars, not just some - Environment drift investigation — prod behaving differently from staging --- ## .env File Structure ### Canonical Layout ```bash # .env.example — committed to git (no values) # .env.local — developer machine (gitignored) # .env.staging — CI/CD or secret manager reference # .env.prod — never on disk; pulled from secret manager at runtime # Application APP_NAME= APP_ENV= # dev | staging | prod APP_PORT=3000 # default port if not set APP_SECRET= # REQUIRED: JWT signing secret (min 32 chars) APP_URL= # REQUIRED: public base URL # Database DATABASE_URL= # REQUIRED: full connection string DATABASE_POOL_MIN=2 DATABASE_POOL_MAX=10 # Auth AUTH_JWT_SECRET= # REQUIRED AUTH_JWT_EXPIRY=3600 # seconds AUTH_REFRESH_SECRET= # REQUIRED # Third-party APIs STRIPE_SECRET_KEY= # REQUIRED in prod STRIPE_WEBHOOK_SECRET= # REQUIRED in prod SENDGRID_API_KEY= # Storage AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_REGION=eu-central-1 AWS_S3_BUCKET= # Monitoring SENTRY_DSN= DD_API_KEY= ``` --- ## .gitignore Patterns Add to your project's `.gitignore`: ```gitignore # Environment files — NEVER commit these .env .env.local .env.development .env.development.local .env.test.local .env.staging .env.staging.local .env.production .env.production.local .env.prod .env.*.local # Secret files *.pem *.key *.p12 *.pfx secrets.json secrets.yaml secrets.yml credentials.json service-account.json # AWS .aws/credentials # Terraform state (may contain secrets) *.tfstate *.tfstate.backup .terraform/ # Kubernetes secrets *-secret.yaml *-secrets.yaml ``` --- ## .env.example Auto-Generation ```bash #!/bin/bash # scripts/gen-env-example.sh # Strips values from .env, preserves keys, defaults, and comments INPUT="-.env" OUTPUT="-.env.example" if [ ! -f "$INPUT" ]; then echo "ERROR: $INPUT not found" exit 1 fi python3 - "$INPUT" "$OUTPUT" << 'PYEOF' import sys, re input_file = sys.argv[1] output_file = sys.argv[2] lines = [] with open(input_file) as f: for line in f: stripped = line.rstrip('\n') # Keep blank lines and comments as-is if stripped == '' or stripped.startswith('#'): lines.append(stripped) continue # Match KEY=VALUE or KEY="VALUE" m = re.match(r'^([A-Z_][A-Z0-9_]*)=(.*)$', stripped) if m: key = m.group(1) value = m.group(2).strip('"\'') # Keep non-sensitive defaults (ports, regions, feature flags) safe_defaults = re.compile( r'^(APP_PORT|APP_ENV|APP_NAME|AWS_REGION|DATABASE_POOL_|LOG_LEVEL|' r'FEATURE_|CACHE_TTL|RATE_LIMIT_|PAGINATION_|TIMEOUT_)', re.I ) sensitive = re.compile( r'(SECRET|KEY|TOKEN|PASSWORD|PASS|CREDENTIAL|DSN|AUTH|PRIVATE|CERT)', re.I ) if safe_defaults.match(key) and value: lines.append(f"{key}={value} # default") else: lines.append(f"{key}=") else: lines.append(stripped) with open(output_file, 'w') as f: f.write('\n'.join(lines) + '\n') print(f"Generated {output_file} from {input_file}") PYEOF ``` Usage: ```bash bash scripts/gen-env-example.sh .env .env.example # Commit .env.example, never .env git add .env.example ``` --- ## Required Variable Validation Script → See references/validation-detection-rotation.md for details ## Secret Manager Integrations ### HashiCorp Vault KV v2 ```bash # Setup export VAULT_ADDR="https://vault.internal.company.com" export VAULT_TOKEN="$(vault login -method=oidc -format=json | jq -r '.auth.client_token')" # Write secrets vault kv put secret/myapp/prod \ DATABASE_URL="postgres://user:pass@host/db" \ APP_SECRET="$(openssl rand -base64 32)" # Read secrets into env eval $(vault kv get -format=json secret/myapp/prod | \ jq -r '.data.data | to_entries[] | "export \(.key)=\(.value)"') # In CI/CD (GitHub Actions) # Use vault-action: hashicorp/vault-action@v2 ``` ### AWS SSM Parameter Store ```bash # Write (SecureString = encrypted with KMS) aws ssm put-parameter \ --name "/myapp/prod/DATABASE_URL" \ --value "postgres://..." \ --type "SecureString" \ --key-id "alias/myapp-secrets" # Read all params for an app/env into shell eval $(aws ssm get-parameters-by-path \ --path "/myapp/prod/" \ --with-decryption \ --query "Parameters[*].[Name,Value]" \ --output text | \ awk '{split($1,a,"/"); print "export " a[length(a)] "=\"" $2 "\""}') # In Node.js at startup # Use @aws-sdk/client-ssm to pull params before server starts ``` ### 1Password CLI ```bash # Authenticate eval $(op signin) # Get a specific field op read "op://MyVault/MyApp Prod/STRIPE_SECRET_KEY" # Export all fields from an item as env vars op item get "MyApp Prod" --format json | \ jq -r '.fields[] | select(.value != null) | "export \(.label)=\"\(.value)\""' | \ grep -E "^export [A-Z_]+" | source /dev/stdin # .env injection op inject -i .env.tpl -o .env # .env.tpl uses {{ op://Vault/Item/field }} syntax ``` ### Doppler ```bash # Setup doppler setup # interactive: select project + config # Run any command with secrets injected doppler run -- node server.js doppler run -- npm run dev # Export to .env (local dev only — never commit output) doppler secrets download --no-file --format env > .env.local # Pull specific secret doppler secrets get DATABASE_URL --plain # Sync to another environment doppler secrets upload --project myapp --config staging < .env.staging.example ``` --- ## Environment Drift Detection Check if staging and prod have the same set of keys (values may differ): ```bash #!/bin/bash # scripts/check-env-drift.sh # Pull key names from both environments (not values) STAGING_KEYS=$(doppler secrets --project myapp --config staging --format json 2>/dev/null | \ jq -r 'keys[]' | sort) PROD_KEYS=$(doppler secrets --project myapp --config prod --format json 2>/dev/null | \ jq -r 'keys[]' | sort) ONLY_IN_STAGING=$(comm -23 <(echo "$STAGING_KEYS") <(echo "$PROD_KEYS")) ONLY_IN_PROD=$(comm -13 <(echo "$STAGING_KEYS") <(echo "$PROD_KEYS")) if [ -n "$ONLY_IN_STAGING" ]; then echo "Keys in STAGING but NOT in PROD:" echo "$ONLY_IN_STAGING" | sed 's/^/ /' fi if [ -n "$ONLY_IN_PROD" ]; then echo "Keys in PROD but NOT in STAGING:" echo "$ONLY_IN_PROD" | sed 's/^/ /' fi if [ -z "$ONLY_IN_STAGING" ] && [ -z "$ONLY_IN_PROD" ]; then echo "✅ No env drift detected — staging and prod have identical key sets" fi ``` --- ## Common Pitfalls - **Committing .env instead of .env.example** — add `.env` to .gitignore on day 1; use pre-commit hooks - **Storing secrets in CI/CD logs** — never `echo $SECRET`; mask vars in CI settings - **Rotating only one place** — secrets often appear in Heroku, Vercel, Docker, K8s, CI — update ALL - **Forgetting to invalidate sessions after JWT secret rotation** — all users will be logged out; communicate this - **Using .env.example with real values** — example files are public; strip everything sensitive - **Not monitoring after rotation** — watch audit logs for 24h after rotation to catch unauthorized old-credential use - **Weak secrets** — `APP_SECRET=mysecret` is not a secret. Use `openssl rand -base64 32` --- ## Best Practices 1. **Secret manager is source of truth** — .env files are for local dev only; never in prod 2. **Rotate on a schedule**, not just after incidents — quarterly minimum for long-lived keys 3. **Principle of least privilege** — each service gets its own API key with minimal permissions 4. **Audit access** — log every secret read in Vault/SSM; alert on anomalous access 5. **Never log secrets** — add log scrubbing middleware that redacts known secret patterns 6. **Use short-lived credentials** — prefer OIDC/instance roles over long-lived access keys 7. **Separate secrets per environment** — never share a key between dev and prod 8. **Document rotation runbooks** — before an incident, not during one FILE:references/validation-detection-rotation.md # env-secrets-manager reference ## Required Variable Validation Script ```bash #!/bin/bash # scripts/validate-env.sh # Run at app startup or in CI before deploy # Exit 1 if any required var is missing or empty set -euo pipefail MISSING=() WARNINGS=() # --- Define required vars by environment --- ALWAYS_REQUIRED=( APP_SECRET APP_URL DATABASE_URL AUTH_JWT_SECRET AUTH_REFRESH_SECRET ) PROD_REQUIRED=( STRIPE_SECRET_KEY STRIPE_WEBHOOK_SECRET SENTRY_DSN ) # --- Check always-required vars --- for var in "ALWAYS_REQUIRED[@]"; do if [ -z "-" ]; then MISSING+=("$var") fi done # --- Check prod-only vars --- if [ "-" = "production" ] || [ "-" = "production" ]; then for var in "PROD_REQUIRED[@]"; do if [ -z "-" ]; then MISSING+=("$var (required in production)") fi done fi # --- Validate format/length constraints --- if [ -n "-" ] && [ #AUTH_JWT_SECRET -lt 32 ]; then WARNINGS+=("AUTH_JWT_SECRET is shorter than 32 chars — insecure") fi if [ -n "-" ]; then if ! echo "$DATABASE_URL" | grep -qE "^(postgres|postgresql|mysql|mongodb|redis)://"; then WARNINGS+=("DATABASE_URL doesn't look like a valid connection string") fi fi if [ -n "-" ]; then if ! [[ "$APP_PORT" =~ ^[0-9]+$ ]] || [ "$APP_PORT" -lt 1 ] || [ "$APP_PORT" -gt 65535 ]; then WARNINGS+=("APP_PORT=$APP_PORT is not a valid port number") fi fi # --- Report --- if [ #WARNINGS[@] -gt 0 ]; then echo "WARNINGS:" for w in "WARNINGS[@]"; do echo " ⚠️ $w" done fi if [ #MISSING[@] -gt 0 ]; then echo "" echo "FATAL: Missing required environment variables:" for var in "MISSING[@]"; do echo " ❌ $var" done echo "" echo "Copy .env.example to .env and fill in missing values." exit 1 fi echo "✅ All required environment variables are set" ``` Node.js equivalent: ```typescript // src/config/validateEnv.ts const required = [ 'APP_SECRET', 'APP_URL', 'DATABASE_URL', 'AUTH_JWT_SECRET', 'AUTH_REFRESH_SECRET', ] const missing = required.filter(key => !process.env[key]) if (missing.length > 0) { console.error('FATAL: Missing required environment variables:', missing) process.exit(1) } if (process.env.AUTH_JWT_SECRET && process.env.AUTH_JWT_SECRET.length < 32) { console.error('FATAL: AUTH_JWT_SECRET must be at least 32 characters') process.exit(1) } export const config = { appSecret: process.env.APP_SECRET!, appUrl: process.env.APP_URL!, databaseUrl: process.env.DATABASE_URL!, jwtSecret: process.env.AUTH_JWT_SECRET!, refreshSecret: process.env.AUTH_REFRESH_SECRET!, stripeKey: process.env.STRIPE_SECRET_KEY, // optional port: parseInt(process.env.APP_PORT ?? '3000', 10), } as const ``` --- ## Secret Leak Detection ### Scan Working Tree ```bash #!/bin/bash # scripts/scan-secrets.sh # Scan staged files and working tree for common secret patterns FAIL=0 check() { local label="$1" local pattern="$2" local matches matches=$(git diff --cached -U0 2>/dev/null | grep "^+" | grep -vE "^(\+\+\+|#|\/\/)" | \ grep -E "$pattern" | grep -v ".env.example" | grep -v "test\|mock\|fixture\|fake" || true) if [ -n "$matches" ]; then echo "SECRET DETECTED [$label]:" echo "$matches" | head -5 FAIL=1 fi } # AWS Access Keys check "AWS Access Key" "AKIA[0-9A-Z]{16}" check "AWS Secret Key" "aws_secret_access_key\s*=\s*['\"]?[A-Za-z0-9/+]{40}" # Stripe check "Stripe Live Key" "sk_live_[0-9a-zA-Z]{24,}" check "Stripe Test Key" "sk_test_[0-9a-zA-Z]{24,}" check "Stripe Webhook" "whsec_[0-9a-zA-Z]{32,}" # JWT / Generic secrets check "Hardcoded JWT" "eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}" check "Generic Secret" "(secret|password|passwd|api_key|apikey|token)\s*[:=]\s*['\"][^'\"]{12,}['\"]" # Private keys check "Private Key Block" "-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----" check "PEM Certificate" "-----BEGIN CERTIFICATE-----" # Connection strings with credentials check "DB Connection" "(postgres|mysql|mongodb)://[^:]+:[^@]+@" check "Redis Auth" "redis://:[^@]+@\|rediss://:[^@]+@" # Google check "Google API Key" "AIza[0-9A-Za-z_-]{35}" check "Google OAuth" "[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com" # GitHub check "GitHub Token" "gh[ps]_[A-Za-z0-9]{36,}" check "GitHub Fine-grained" "github_pat_[A-Za-z0-9_]{82}" # Slack check "Slack Token" "xox[baprs]-[0-9A-Za-z]{10,}" check "Slack Webhook" "https://hooks\.slack\.com/services/[A-Z0-9]{9,}/[A-Z0-9]{9,}/[A-Za-z0-9]{24,}" # Twilio check "Twilio SID" "AC[a-z0-9]{32}" check "Twilio Token" "SK[a-z0-9]{32}" if [ $FAIL -eq 1 ]; then echo "" echo "BLOCKED: Secrets detected in staged changes." echo "Remove secrets before committing. Use environment variables instead." echo "If this is a false positive, add it to .secretsignore or use:" echo " git commit --no-verify (only if you're 100% certain it's safe)" exit 1 fi echo "No secrets detected in staged changes." ``` ### Scan Git History (post-incident) ```bash #!/bin/bash # scripts/scan-history.sh — scan entire git history for leaked secrets PATTERNS=( "AKIA[0-9A-Z]{16}" "sk_live_[0-9a-zA-Z]{24}" "sk_test_[0-9a-zA-Z]{24}" "-----BEGIN.*PRIVATE KEY-----" "AIza[0-9A-Za-z_-]{35}" "ghp_[A-Za-z0-9]{36}" "xox[baprs]-[0-9A-Za-z]{10,}" ) for pattern in "PATTERNS[@]"; do echo "Scanning for: $pattern" git log --all -p --no-color 2>/dev/null | \ grep -n "$pattern" | \ grep "^+" | \ grep -v "^+++" | \ head -10 done # Alternative: use truffleHog or gitleaks for comprehensive scanning # gitleaks detect --source . --log-opts="--all" # trufflehog git file://. --only-verified ``` --- ## Pre-commit Hook Installation ```bash #!/bin/bash # Install the pre-commit hook HOOK_PATH=".git/hooks/pre-commit" cat > "$HOOK_PATH" << 'HOOK' #!/bin/bash # Pre-commit: scan for secrets before every commit SCRIPT="scripts/scan-secrets.sh" if [ -f "$SCRIPT" ]; then bash "$SCRIPT" else # Inline fallback if script not present if git diff --cached -U0 | grep "^+" | grep -qE "AKIA[0-9A-Z]{16}|sk_live_|-----BEGIN.*PRIVATE KEY"; then echo "BLOCKED: Possible secret detected in staged changes." exit 1 fi fi HOOK chmod +x "$HOOK_PATH" echo "Pre-commit hook installed at $HOOK_PATH" ``` Using `pre-commit` framework (recommended for teams): ```yaml # .pre-commit-config.yaml repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.18.0 hooks: - id: gitleaks - repo: local hooks: - id: validate-env-example name: "check-envexample-is-up-to-date" language: script entry: bash scripts/check-env-example.sh pass_filenames: false ``` --- ## Credential Rotation Workflow When a secret is leaked or compromised: ### Step 1 — Detect & Confirm ```bash # Confirm which secret was exposed git log --all -p --no-color | grep -A2 -B2 "AKIA\|sk_live_\|SECRET" # Check if secret is in any open PRs gh pr list --state open | while read pr; do gh pr diff $(echo $pr | awk '{print $1}') | grep -E "AKIA|sk_live_" && echo "Found in PR: $pr" done ``` ### Step 2 — Identify Exposure Window ```bash # Find first commit that introduced the secret git log --all -p --no-color -- "*.env" "*.json" "*.yaml" "*.ts" "*.py" | \ grep -B 10 "THE_LEAKED_VALUE" | grep "^commit" | tail -1 # Get commit date git show --format="%ci" COMMIT_HASH | head -1 # Check if secret appears in public repos (GitHub) gh api search/code -X GET -f q="THE_LEAKED_VALUE" | jq '.total_count, .items[].html_url' ``` ### Step 3 — Rotate Credential Per service — rotate immediately: - **AWS**: IAM console → delete access key → create new → update everywhere - **Stripe**: Dashboard → Developers → API keys → Roll key - **GitHub PAT**: Settings → Developer Settings → Personal access tokens → Revoke → Create new - **DB password**: `ALTER USER app_user PASSWORD 'new-strong-password-here';` - **JWT secret**: Rotate key (all existing sessions invalidated — users re-login) ### Step 4 — Update All Environments ```bash # Update secret manager (source of truth) # Then redeploy to pull new values # Vault KV v2 vault kv put secret/myapp/prod \ STRIPE_SECRET_KEY="sk_live_NEW..." \ APP_SECRET="new-secret-here" # AWS SSM aws ssm put-parameter \ --name "/myapp/prod/STRIPE_SECRET_KEY" \ --value "sk_live_NEW..." \ --type "SecureString" \ --overwrite # 1Password op item edit "MyApp Prod" \ --field "STRIPE_SECRET_KEY[password]=sk_live_NEW..." # Doppler doppler secrets set STRIPE_SECRET_KEY="sk_live_NEW..." --project myapp --config prod ``` ### Step 5 — Remove from Git History ```bash # WARNING: rewrites history — coordinate with team first git filter-repo --path-glob "*.env" --invert-paths # Or remove specific string from all commits git filter-repo --replace-text <(echo "LEAKED_VALUE==>REDACTED") # Force push all branches (requires team coordination + force push permissions) git push origin --force --all # Notify all developers to re-clone ``` ### Step 6 — Verify ```bash # Confirm secret no longer in history git log --all -p | grep "LEAKED_VALUE" | wc -l # should be 0 # Test new credentials work curl -H "Authorization: Bearer $NEW_TOKEN" https://api.service.com/test # Monitor for unauthorized usage of old credential (check service audit logs) ``` ---
Database Schema Designer
---
name: "database-schema-designer"
description: "Database Schema Designer"
---
# Database Schema Designer
**Tier:** POWERFUL
**Category:** Engineering
**Domain:** Data Architecture / Backend
---
## Overview
Design relational database schemas from requirements and generate migrations, TypeScript/Python types, seed data, RLS policies, and indexes. Handles multi-tenancy, soft deletes, audit trails, versioning, and polymorphic associations.
## Core Capabilities
- **Schema design** — normalize requirements into tables, relationships, constraints
- **Migration generation** — Drizzle, Prisma, TypeORM, Alembic
- **Type generation** — TypeScript interfaces, Python dataclasses/Pydantic models
- **RLS policies** — Row-Level Security for multi-tenant apps
- **Index strategy** — composite indexes, partial indexes, covering indexes
- **Seed data** — realistic test data generation
- **ERD generation** — Mermaid diagram from schema
---
## When to Use
- Designing a new feature that needs database tables
- Reviewing a schema for performance or normalization issues
- Adding multi-tenancy to an existing schema
- Generating TypeScript types from a Prisma schema
- Planning a schema migration for a breaking change
---
## Schema Design Process
### Step 1: Requirements → Entities
Given requirements:
> "Users can create projects. Each project has tasks. Tasks can have labels. Tasks can be assigned to users. We need a full audit trail."
Extract entities:
```
User, Project, Task, Label, TaskLabel (junction), TaskAssignment, AuditLog
```
### Step 2: Identify Relationships
```
User 1──* Project (owner)
Project 1──* Task
Task *──* Label (via TaskLabel)
Task *──* User (via TaskAssignment)
User 1──* AuditLog
```
### Step 3: Add Cross-cutting Concerns
- Multi-tenancy: add `organization_id` to all tenant-scoped tables
- Soft deletes: add `deleted_at TIMESTAMPTZ` instead of hard deletes
- Audit trail: add `created_by`, `updated_by`, `created_at`, `updated_at`
- Versioning: add `version INTEGER` for optimistic locking
---
## Full Schema Example (Task Management SaaS)
→ See references/full-schema-examples.md for details
## Row-Level Security (RLS) Policies
```sql
-- Enable RLS
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Create app role
CREATE ROLE app_user;
-- Users can only see tasks in their organization's projects
CREATE POLICY tasks_org_isolation ON tasks
FOR ALL TO app_user
USING (
project_id IN (
SELECT p.id FROM projects p
JOIN organization_members om ON om.organization_id = p.organization_id
WHERE om.user_id = current_setting('app.current_user_id')::text
)
);
-- Soft delete: never show deleted records
CREATE POLICY tasks_no_deleted ON tasks
FOR SELECT TO app_user
USING (deleted_at IS NULL);
-- Only task creator or admin can delete
CREATE POLICY tasks_delete_policy ON tasks
FOR DELETE TO app_user
USING (
created_by_id = current_setting('app.current_user_id')::text
OR EXISTS (
SELECT 1 FROM organization_members om
JOIN projects p ON p.organization_id = om.organization_id
WHERE p.id = tasks.project_id
AND om.user_id = current_setting('app.current_user_id')::text
AND om.role IN ('owner', 'admin')
)
);
-- Set user context (call at start of each request)
SELECT set_config('app.current_user_id', $1, true);
```
---
## Seed Data Generation
```typescript
// db/seed.ts
import { faker } from '@faker-js/faker'
import { db } from './client'
import { organizations, users, projects, tasks } from './schema'
import { createId } from '@paralleldrive/cuid2'
import { hashPassword } from '../src/lib/auth'
async function seed() {
console.log('Seeding database...')
// Create org
const [org] = await db.insert(organizations).values({
id: createId(),
name: "acme-corp",
slug: 'acme',
plan: 'growth',
}).returning()
// Create users
const adminUser = await db.insert(users).values({
id: createId(),
email: '[email protected]',
name: "alice-admin",
passwordHash: await hashPassword('password123'),
}).returning().then(r => r[0])
// Create projects
const projectsData = Array.from({ length: 3 }, () => ({
id: createId(),
organizationId: org.id,
ownerId: adminUser.id,
name: "fakercompanycatchphrase"
description: faker.lorem.paragraph(),
status: 'active' as const,
}))
const createdProjects = await db.insert(projects).values(projectsData).returning()
// Create tasks for each project
for (const project of createdProjects) {
const tasksData = Array.from({ length: faker.number.int({ min: 5, max: 20 }) }, (_, i) => ({
id: createId(),
projectId: project.id,
title: faker.hacker.phrase(),
description: faker.lorem.sentences(2),
status: faker.helpers.arrayElement(['todo', 'in_progress', 'done'] as const),
priority: faker.helpers.arrayElement(['low', 'medium', 'high'] as const),
position: i * 1000,
createdById: adminUser.id,
updatedById: adminUser.id,
}))
await db.insert(tasks).values(tasksData)
}
console.log(`✅ Seeded: 1 org, projectsData.length projects, tasks`)
}
seed().catch(console.error).finally(() => process.exit(0))
```
---
## ERD Generation (Mermaid)
```
erDiagram
Organization ||--o{ OrganizationMember : has
Organization ||--o{ Project : owns
User ||--o{ OrganizationMember : joins
User ||--o{ Task : "created by"
Project ||--o{ Task : contains
Task ||--o{ TaskAssignment : has
Task ||--o{ TaskLabel : has
Task ||--o{ Comment : has
Task ||--o{ Attachment : has
Label ||--o{ TaskLabel : "applied to"
User ||--o{ TaskAssignment : assigned
Organization {
string id PK
string name
string slug
string plan
}
Task {
string id PK
string project_id FK
string title
string status
string priority
timestamp due_date
timestamp deleted_at
int version
}
```
Generate from Prisma:
```bash
npx prisma-erd-generator
# or: npx @dbml/cli prisma2dbml -i schema.prisma | npx dbml-to-mermaid
```
---
## Common Pitfalls
- **Soft delete without index** — `WHERE deleted_at IS NULL` without index = full scan
- **Missing composite indexes** — `WHERE org_id = ? AND status = ?` needs a composite index
- **Mutable surrogate keys** — never use email or slug as PK; use UUID/CUID
- **Non-nullable without default** — adding a NOT NULL column to existing table requires default or migration plan
- **No optimistic locking** — concurrent updates overwrite each other; add `version` column
- **RLS not tested** — always test RLS with a non-superuser role
---
## Best Practices
1. **Timestamps everywhere** — `created_at`, `updated_at` on every table
2. **Soft deletes for auditable data** — `deleted_at` instead of DELETE
3. **Audit log for compliance** — log before/after JSON for regulated domains
4. **UUIDs or CUIDs as PKs** — avoid sequential integer leakage
5. **Index foreign keys** — every FK column should have an index
6. **Partial indexes** — use `WHERE deleted_at IS NULL` for active-only queries
7. **RLS over application-level filtering** — database enforces tenancy, not just app code
FILE:references/full-schema-examples.md
# database-schema-designer reference
## Full Schema Example (Task Management SaaS)
### Prisma Schema
```prisma
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ── Multi-tenancy ─────────────────────────────────────────────────────────────
model Organization {
id String @id @default(cuid())
name String
slug String @unique
plan Plan @default(FREE)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
users OrganizationMember[]
projects Project[]
auditLogs AuditLog[]
@@map("organizations")
}
model OrganizationMember {
id String @id @default(cuid())
organizationId String @map("organization_id")
userId String @map("user_id")
role OrgRole @default(MEMBER)
joinedAt DateTime @default(now()) @map("joined_at")
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([organizationId, userId])
@@index([userId])
@@map("organization_members")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
avatarUrl String? @map("avatar_url")
passwordHash String? @map("password_hash")
emailVerifiedAt DateTime? @map("email_verified_at")
lastLoginAt DateTime? @map("last_login_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
memberships OrganizationMember[]
ownedProjects Project[] @relation("ProjectOwner")
assignedTasks TaskAssignment[]
comments Comment[]
auditLogs AuditLog[]
@@map("users")
}
// ── Core entities ─────────────────────────────────────────────────────────────
model Project {
id String @id @default(cuid())
organizationId String @map("organization_id")
ownerId String @map("owner_id")
name String
description String?
status ProjectStatus @default(ACTIVE)
settings Json @default("{}")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
organization Organization @relation(fields: [organizationId], references: [id])
owner User @relation("ProjectOwner", fields: [ownerId], references: [id])
tasks Task[]
labels Label[]
@@index([organizationId])
@@index([organizationId, status])
@@index([deletedAt])
@@map("projects")
}
model Task {
id String @id @default(cuid())
projectId String @map("project_id")
title String
description String?
status TaskStatus @default(TODO)
priority Priority @default(MEDIUM)
dueDate DateTime? @map("due_date")
position Float @default(0) // For drag-and-drop ordering
version Int @default(1) // Optimistic locking
createdById String @map("created_by_id")
updatedById String @map("updated_by_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
project Project @relation(fields: [projectId], references: [id])
assignments TaskAssignment[]
labels TaskLabel[]
comments Comment[]
attachments Attachment[]
@@index([projectId])
@@index([projectId, status])
@@index([projectId, deletedAt])
@@index([dueDate], where: { deletedAt: null }) // Partial index
@@map("tasks")
}
// ── Polymorphic attachments ───────────────────────────────────────────────────
model Attachment {
id String @id @default(cuid())
// Polymorphic association
entityType String @map("entity_type") // "task" | "comment"
entityId String @map("entity_id")
filename String
mimeType String @map("mime_type")
sizeBytes Int @map("size_bytes")
storageKey String @map("storage_key") // S3 key
uploadedById String @map("uploaded_by_id")
createdAt DateTime @default(now()) @map("created_at")
// Only one concrete relation (task) — polymorphic handled at app level
task Task? @relation(fields: [entityId], references: [id], map: "attachment_task_fk")
@@index([entityType, entityId])
@@map("attachments")
}
// ── Audit trail ───────────────────────────────────────────────────────────────
model AuditLog {
id String @id @default(cuid())
organizationId String @map("organization_id")
userId String? @map("user_id")
action String // "task.created", "task.status_changed"
entityType String @map("entity_type")
entityId String @map("entity_id")
before Json? // Previous state
after Json? // New state
ipAddress String? @map("ip_address")
userAgent String? @map("user_agent")
createdAt DateTime @default(now()) @map("created_at")
organization Organization @relation(fields: [organizationId], references: [id])
user User? @relation(fields: [userId], references: [id])
@@index([organizationId, createdAt(sort: Desc)])
@@index([entityType, entityId])
@@index([userId])
@@map("audit_logs")
}
enum Plan { FREE STARTER GROWTH ENTERPRISE }
enum OrgRole { OWNER ADMIN MEMBER VIEWER }
enum ProjectStatus { ACTIVE ARCHIVED }
enum TaskStatus { TODO IN_PROGRESS IN_REVIEW DONE CANCELLED }
enum Priority { LOW MEDIUM HIGH CRITICAL }
```
---
### Drizzle Schema (TypeScript)
```typescript
// db/schema.ts
import {
pgTable, text, timestamp, integer, boolean,
varchar, jsonb, real, pgEnum, uniqueIndex, index,
} from 'drizzle-orm/pg-core'
import { createId } from '@paralleldrive/cuid2'
export const taskStatusEnum = pgEnum('task_status', [
'todo', 'in_progress', 'in_review', 'done', 'cancelled'
])
export const priorityEnum = pgEnum('priority', ['low', 'medium', 'high', 'critical'])
export const tasks = pgTable('tasks', {
id: text('id').primaryKey().$defaultFn(() => createId()),
projectId: text('project_id').notNull().references(() => projects.id),
title: varchar('title', { length: 500 }).notNull(),
description: text('description'),
status: taskStatusEnum('status').notNull().default('todo'),
priority: priorityEnum('priority').notNull().default('medium'),
dueDate: timestamp('due_date', { withTimezone: true }),
position: real('position').notNull().default(0),
version: integer('version').notNull().default(1),
createdById: text('created_by_id').notNull().references(() => users.id),
updatedById: text('updated_by_id').notNull().references(() => users.id),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
deletedAt: timestamp('deleted_at', { withTimezone: true }),
}, (table) => ({
projectIdx: index('tasks_project_id_idx').on(table.projectId),
projectStatusIdx: index('tasks_project_status_idx').on(table.projectId, table.status),
}))
// Infer TypeScript types
export type Task = typeof tasks.$inferSelect
export type NewTask = typeof tasks.$inferInsert
```
---
### Alembic Migration (Python / SQLAlchemy)
```python
# alembic/versions/20260301_create_tasks.py
"""Create tasks table
Revision ID: a1b2c3d4e5f6
Revises: previous_revision
Create Date: 2026-03-01 12:00:00
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = 'a1b2c3d4e5f6'
down_revision = 'previous_revision'
def upgrade() -> None:
# Create enums
task_status = postgresql.ENUM(
'todo', 'in_progress', 'in_review', 'done', 'cancelled',
name='task_status'
)
task_status.create(op.get_bind())
op.create_table(
'tasks',
sa.Column('id', sa.Text(), primary_key=True),
sa.Column('project_id', sa.Text(), sa.ForeignKey('projects.id'), nullable=False),
sa.Column('title', sa.VARCHAR(500), nullable=False),
sa.Column('description', sa.Text()),
sa.Column('status', postgresql.ENUM('todo', 'in_progress', 'in_review', 'done', 'cancelled', name='task_status', create_type=False), nullable=False, server_default='todo'),
sa.Column('priority', sa.Text(), nullable=False, server_default='medium'),
sa.Column('due_date', sa.TIMESTAMP(timezone=True)),
sa.Column('position', sa.Float(), nullable=False, server_default='0'),
sa.Column('version', sa.Integer(), nullable=False, server_default='1'),
sa.Column('created_by_id', sa.Text(), sa.ForeignKey('users.id'), nullable=False),
sa.Column('updated_by_id', sa.Text(), sa.ForeignKey('users.id'), nullable=False),
sa.Column('created_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text('NOW()')),
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text('NOW()')),
sa.Column('deleted_at', sa.TIMESTAMP(timezone=True)),
)
# Indexes
op.create_index('tasks_project_id_idx', 'tasks', ['project_id'])
op.create_index('tasks_project_status_idx', 'tasks', ['project_id', 'status'])
# Partial index for active tasks only
op.create_index(
'tasks_due_date_active_idx',
'tasks', ['due_date'],
postgresql_where=sa.text('deleted_at IS NULL')
)
def downgrade() -> None:
op.drop_table('tasks')
op.execute("DROP TYPE IF EXISTS task_status")
```
---
Codebase Onboarding
---
name: "codebase-onboarding"
description: "Codebase Onboarding"
---
# Codebase Onboarding
**Tier:** POWERFUL
**Category:** Engineering
**Domain:** Documentation / Developer Experience
---
## Overview
Analyze a codebase and generate comprehensive onboarding documentation tailored to your audience. Produces architecture overviews, key file maps, local setup guides, common task runbooks, debugging guides, and contribution guidelines. Outputs to Markdown, Notion, or Confluence.
## Core Capabilities
- **Architecture overview** — tech stack, system boundaries, data flow diagrams
- **Key file map** — what's important and why, with annotations
- **Local setup guide** — step-by-step from clone to running tests
- **Common developer tasks** — how to add a route, run migrations, create a component
- **Debugging guide** — common errors, log locations, useful queries
- **Contribution guidelines** — branch strategy, PR process, code style
- **Audience-aware output** — junior, senior, or contractor mode
---
## When to Use
- Onboarding a new team member or contractor
- After a major refactor that made existing docs stale
- Before open-sourcing a project
- Creating a team wiki page for a service
- Self-documenting before a long vacation
---
## Codebase Analysis Commands
Run these before generating docs to gather facts:
```bash
# Project overview
cat package.json | jq '{name, version, scripts, dependencies: (.dependencies | keys), devDependencies: (.devDependencies | keys)}'
# Directory structure (top 2 levels)
find . -maxdepth 2 -not -path '*/node_modules/*' -not -path '*/.git/*' -not -path '*/.next/*' | sort | head -60
# Largest files (often core modules)
find src/ -name "*.ts" -not -path "*/test*" -exec wc -l {} + | sort -rn | head -20
# All routes (Next.js App Router)
find app/ -name "route.ts" -o -name "page.tsx" | sort
# All routes (Express)
grep -rn "router\.\(get\|post\|put\|patch\|delete\)" src/routes/ --include="*.ts"
# Recent major changes
git log --oneline --since="90 days ago" | grep -E "feat|refactor|breaking"
# Top contributors
git shortlog -sn --no-merges | head -10
# Test coverage summary
pnpm test:ci --coverage 2>&1 | tail -20
```
---
## Generated Documentation Template
### README.md — Full Template
```markdown
# [Project Name]
> One-sentence description of what this does and who uses it.
[](https://github.com/org/repo/actions/workflows/ci.yml)
[](https://codecov.io/gh/org/repo)
## What is this?
[2-3 sentences: problem it solves, who uses it, current state]
**Live:** https://myapp.com
**Staging:** https://staging.myapp.com
**Docs:** https://docs.myapp.com
---
## Quick Start
### Prerequisites
| Tool | Version | Install |
|------|---------|---------|
| Node.js | 20+ | `nvm install 20` |
| pnpm | 8+ | `npm i -g pnpm` |
| Docker | 24+ | [docker.com](https://docker.com) |
| PostgreSQL | 16+ | via Docker (see below) |
### Setup (5 minutes)
```bash
# 1. Clone
git clone https://github.com/org/repo
cd repo
# 2. Install dependencies
pnpm install
# 3. Start infrastructure
docker compose up -d # Starts Postgres, Redis
# 4. Environment
cp .env.example .env
# Edit .env — ask a teammate for real values or see Vault
# 5. Database setup
pnpm db:migrate # Run migrations
pnpm db:seed # Optional: load test data
# 6. Start dev server
pnpm dev # → http://localhost:3000
# 7. Verify
pnpm test # Should be all green
```
### Verify it works
- [ ] `http://localhost:3000` loads the app
- [ ] `http://localhost:3000/api/health` returns `{"status":"ok"}`
- [ ] `pnpm test` passes
---
## Architecture
### System Overview
```
Browser / Mobile
│
▼
[Next.js App] ←──── [Auth: NextAuth]
│
├──→ [PostgreSQL] (primary data store)
├──→ [Redis] (sessions, job queue)
└──→ [S3] (file uploads)
Background:
[BullMQ workers] ←── Redis queue
└──→ [External APIs: Stripe, SendGrid]
```
### Tech Stack
| Layer | Technology | Why |
|-------|-----------|-----|
| Frontend | Next.js 14 (App Router) | SSR, file-based routing |
| Styling | Tailwind CSS + shadcn/ui | Rapid UI development |
| API | Next.js Route Handlers | Co-located with frontend |
| Database | PostgreSQL 16 | Relational, RLS for multi-tenancy |
| ORM | Drizzle ORM | Type-safe, lightweight |
| Auth | NextAuth v5 | OAuth + email/password |
| Queue | BullMQ + Redis | Background jobs |
| Storage | AWS S3 | File uploads |
| Email | SendGrid | Transactional email |
| Payments | Stripe | Subscriptions |
| Deployment | Vercel (app) + Railway (workers) | |
| Monitoring | Sentry + Datadog | |
---
## Key Files
| Path | Purpose |
|------|---------|
| `app/` | Next.js App Router — pages and API routes |
| `app/api/` | API route handlers |
| `app/(auth)/` | Auth pages (login, register, reset) |
| `app/(app)/` | Protected app pages |
| `src/db/` | Database schema, migrations, client |
| `src/db/schema.ts` | **Drizzle schema — single source of truth** |
| `src/lib/` | Shared utilities (auth, email, stripe) |
| `src/lib/auth.ts` | **Auth configuration — read this first** |
| `src/components/` | Reusable React components |
| `src/hooks/` | Custom React hooks |
| `src/types/` | Shared TypeScript types |
| `workers/` | BullMQ background job processors |
| `emails/` | React Email templates |
| `tests/` | Test helpers, factories, integration tests |
| `.env.example` | All env vars with descriptions |
| `docker-compose.yml` | Local infrastructure |
---
## Common Developer Tasks
### Add a new API endpoint
```bash
# 1. Create route handler
touch app/api/my-resource/route.ts
```
```typescript
// app/api/my-resource/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { db } from '@/db/client'
export async function GET(req: NextRequest) {
const session = await auth()
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const data = await db.query.myResource.findMany({
where: (r, { eq }) => eq(r.userId, session.user.id),
})
return NextResponse.json({ data })
}
```
```bash
# 2. Add tests
touch tests/api/my-resource.test.ts
# 3. Add to OpenAPI spec (if applicable)
pnpm generate:openapi
```
### Run a database migration
```bash
# Create migration
pnpm db:generate # Generates SQL from schema changes
# Review the generated SQL
cat drizzle/migrations/0001_my_change.sql
# Apply
pnpm db:migrate
# Roll back (manual — inspect generated SQL and revert)
psql $DATABASE_URL -f scripts/rollback_0001.sql
```
### Add a new email template
```bash
# 1. Create template
touch emails/my-email.tsx
# 2. Preview in browser
pnpm email:preview
# 3. Send in code
import { sendEmail } from '@/lib/email'
await sendEmail({
to: user.email,
subject: 'Subject line',
template: 'my-email',
props: { name: "username"
})
```
### Add a background job
```typescript
// 1. Define job in workers/jobs/my-job.ts
import { Queue, Worker } from 'bullmq'
import { redis } from '@/lib/redis'
export const myJobQueue = new Queue('my-job', { connection: redis })
export const myJobWorker = new Worker('my-job', async (job) => {
const { userId, data } = job.data
// do work
}, { connection: redis })
// 2. Enqueue
await myJobQueue.add('process', { userId, data }, {
attempts: 3,
backoff: { type: 'exponential', delay: 1000 },
})
```
---
## Debugging Guide
### Common Errors
**`Error: DATABASE_URL is not set`**
```bash
# Check your .env file exists and has the var
cat .env | grep DATABASE_URL
# Start Postgres if not running
docker compose up -d postgres
```
**`PrismaClientKnownRequestError: P2002 Unique constraint failed`**
```
User already exists with that email. Check: is this a duplicate registration?
Run: SELECT * FROM users WHERE email = '[email protected]';
```
**`Error: JWT expired`**
```bash
# Dev: extend token TTL in .env
JWT_EXPIRES_IN=30d
# Check clock skew between server and client
date && docker exec postgres date
```
**`500 on /api/*` in local dev**
```bash
# 1. Check terminal for stack trace
# 2. Check database connectivity
psql $DATABASE_URL -c "SELECT 1"
# 3. Check Redis
redis-cli ping
# 4. Check logs
pnpm dev 2>&1 | grep -E "error|Error|ERROR"
```
### Useful SQL Queries
```sql
-- Find slow queries (requires pg_stat_statements)
SELECT query, mean_exec_time, calls, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
-- Check active connections
SELECT count(*), state FROM pg_stat_activity GROUP BY state;
-- Find bloated tables
SELECT relname, n_dead_tup, n_live_tup,
round(n_dead_tup::numeric/nullif(n_live_tup,0)*100, 2) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
```
### Debug Authentication
```bash
# Decode a JWT (no secret needed for header/payload)
echo "YOUR_JWT" | cut -d. -f2 | base64 -d | jq .
# Check session in DB
psql $DATABASE_URL -c "SELECT * FROM sessions WHERE user_id = 'usr_...' ORDER BY expires_at DESC LIMIT 5;"
```
### Log Locations
| Environment | Logs |
|-------------|------|
| Local dev | Terminal running `pnpm dev` |
| Vercel production | Vercel dashboard → Logs |
| Workers (Railway) | Railway dashboard → Deployments → Logs |
| Database | `docker logs postgres` (local) |
| Background jobs | `pnpm worker:dev` terminal |
---
## Contribution Guidelines
### Branch Strategy
```
main → production (protected, requires PR + CI)
└── feature/PROJ-123-short-desc
└── fix/PROJ-456-bug-description
└── chore/update-dependencies
```
### PR Requirements
- [ ] Branch name includes ticket ID (e.g., `feature/PROJ-123-...`)
- [ ] PR description explains the why
- [ ] All CI checks pass
- [ ] Test coverage doesn't decrease
- [ ] Self-reviewed (read your own diff before requesting review)
- [ ] Screenshots/video for UI changes
### Commit Convention
```
feat(scope): short description → new feature
fix(scope): short description → bug fix
chore: update dependencies → maintenance
docs: update API reference → documentation
```
### Code Style
```bash
# Lint + format
pnpm lint
pnpm format
# Type check
pnpm typecheck
# All checks (run before pushing)
pnpm validate
```
---
## Audience-Specific Notes
### For Junior Developers
- Start with `src/lib/auth.ts` to understand authentication
- Read existing tests in `tests/api/` — they document expected behavior
- Ask before touching anything in `src/db/schema.ts` — schema changes affect everyone
- Use `pnpm db:seed` to get realistic local data
### For Senior Engineers / Tech Leads
- Architecture decisions are documented in `docs/adr/` (Architecture Decision Records)
- Performance benchmarks: `pnpm bench` — baseline is in `tests/benchmarks/baseline.json`
- Security model: RLS policies in `src/db/rls.sql`, enforced at DB level
- Scaling notes: `docs/scaling.md`
### For Contractors
- Scope is limited to `src/features/[your-feature]/` unless discussed
- Never push directly to `main`
- All external API calls go through `src/lib/` wrappers (for mocking in tests)
- Time estimates: log in Linear ticket comments daily
---
## Output Formats
→ See references/output-format-templates.md for details
## Common Pitfalls
- **Docs written once, never updated** — add doc updates to PR checklist
- **Missing local setup step** — test setup instructions on a fresh machine quarterly
- **No error troubleshooting** — debugging section is the most valuable part for new hires
- **Too much detail for contractors** — they need task-specific, not architecture-deep docs
- **No screenshots** — UI flows need screenshots; they go stale but are still valuable
- **Skipping the "why"** — document why decisions were made, not just what was decided
---
## Best Practices
1. **Keep setup under 10 minutes** — if it takes longer, fix the setup, not the docs
2. **Test the docs** — have a new hire follow them literally, fix every gap they hit
3. **Link, don't repeat** — link to ADRs, issues, and external docs instead of duplicating
4. **Update in the same PR** — docs changes alongside code changes
5. **Version-specific notes** — call out things that changed in recent versions
6. **Runbooks over theory** — "run this command" beats "the system uses Redis for..."
FILE:references/output-format-templates.md
# codebase-onboarding reference
## Output Formats
### Notion Export
```javascript
// Use Notion API to create onboarding page
const { Client } = require('@notionhq/client')
const notion = new Client({ auth: process.env.NOTION_TOKEN })
const blocks = markdownToNotionBlocks(onboardingMarkdown) // use notion-to-md
await notion.pages.create({
parent: { page_id: ONBOARDING_PARENT_PAGE_ID },
properties: { title: { title: [{ text: { content: 'Engineer Onboarding — MyApp' } }] } },
children: blocks,
})
```
### Confluence Export
```bash
# Using confluence-cli or REST API
curl -X POST \
-H "Content-Type: application/json" \
-u "[email protected]:$CONFLUENCE_TOKEN" \
"https://yourorg.atlassian.net/wiki/rest/api/content" \
-d '{
"type": "page",
"title": "Codebase Onboarding",
"space": {"key": "ENG"},
"body": {
"storage": {
"value": "<p>Generated content...</p>",
"representation": "storage"
}
}
}'
```
---
CI/CD Pipeline Builder
---
name: "ci-cd-pipeline-builder"
description: "CI/CD Pipeline Builder"
---
# CI/CD Pipeline Builder
**Tier:** POWERFUL
**Category:** Engineering
**Domain:** DevOps / Automation
## Overview
Use this skill to generate pragmatic CI/CD pipelines from detected project stack signals, not guesswork. It focuses on fast baseline generation, repeatable checks, and environment-aware deployment stages.
## Core Capabilities
- Detect language/runtime/tooling from repository files
- Recommend CI stages (`lint`, `test`, `build`, `deploy`)
- Generate GitHub Actions or GitLab CI starter pipelines
- Include caching and matrix strategy based on detected stack
- Emit machine-readable detection output for automation
- Keep pipeline logic aligned with project lockfiles and build commands
## When to Use
- Bootstrapping CI for a new repository
- Replacing brittle copied pipeline files
- Migrating between GitHub Actions and GitLab CI
- Auditing whether pipeline steps match actual stack
- Creating a reproducible baseline before custom hardening
## Key Workflows
### 1. Detect Stack
```bash
python3 scripts/stack_detector.py --repo . --format text
python3 scripts/stack_detector.py --repo . --format json > detected-stack.json
```
Supports input via stdin or `--input` file for offline analysis payloads.
### 2. Generate Pipeline From Detection
```bash
python3 scripts/pipeline_generator.py \
--input detected-stack.json \
--platform github \
--output .github/workflows/ci.yml \
--format text
```
Or end-to-end from repo directly:
```bash
python3 scripts/pipeline_generator.py --repo . --platform gitlab --output .gitlab-ci.yml
```
### 3. Validate Before Merge
1. Confirm commands exist in project (`test`, `lint`, `build`).
2. Run generated pipeline locally where possible.
3. Ensure required secrets/env vars are documented.
4. Keep deploy jobs gated by protected branches/environments.
### 4. Add Deployment Stages Safely
- Start with CI-only (`lint/test/build`).
- Add staging deploy with explicit environment context.
- Add production deploy with manual gate/approval.
- Keep rollout/rollback commands explicit and auditable.
## Script Interfaces
- `python3 scripts/stack_detector.py --help`
- Detects stack signals from repository files
- Reads optional JSON input from stdin/`--input`
- `python3 scripts/pipeline_generator.py --help`
- Generates GitHub/GitLab YAML from detection payload
- Writes to stdout or `--output`
## Common Pitfalls
1. Copying a Node pipeline into Python/Go repos
2. Enabling deploy jobs before stable tests
3. Forgetting dependency cache keys
4. Running expensive matrix builds for every trivial branch
5. Missing branch protections around prod deploy jobs
6. Hardcoding secrets in YAML instead of CI secret stores
## Best Practices
1. Detect stack first, then generate pipeline.
2. Keep generated baseline under version control.
3. Add one optimization at a time (cache, matrix, split jobs).
4. Require green CI before deployment jobs.
5. Use protected environments for production credentials.
6. Regenerate pipeline when stack changes significantly.
## References
- [references/github-actions-templates.md](references/github-actions-templates.md)
- [references/gitlab-ci-templates.md](references/gitlab-ci-templates.md)
- [references/deployment-gates.md](references/deployment-gates.md)
- [README.md](README.md)
## Detection Heuristics
The stack detector prioritizes deterministic file signals over heuristics:
- Lockfiles determine package manager preference
- Language manifests determine runtime families
- Script commands (if present) drive lint/test/build commands
- Missing scripts trigger conservative placeholder commands
## Generation Strategy
Start with a minimal, reliable pipeline:
1. Checkout and setup runtime
2. Install dependencies with cache strategy
3. Run lint, test, build in separate steps
4. Publish artifacts only after passing checks
Then layer advanced behavior (matrix builds, security scans, deploy gates).
## Platform Decision Notes
- GitHub Actions for tight GitHub ecosystem integration
- GitLab CI for integrated SCM + CI in self-hosted environments
- Keep one canonical pipeline source per repo to reduce drift
## Validation Checklist
1. Generated YAML parses successfully.
2. All referenced commands exist in the repo.
3. Cache strategy matches package manager.
4. Required secrets are documented, not embedded.
5. Branch/protected-environment rules match org policy.
## Scaling Guidance
- Split long jobs by stage when runtime exceeds 10 minutes.
- Introduce test matrix only when compatibility truly requires it.
- Separate deploy jobs from CI jobs to keep feedback fast.
- Track pipeline duration and flakiness as first-class metrics.
FILE:README.md
# CI/CD Pipeline Builder
Detects your repository stack and generates practical CI pipeline templates for GitHub Actions and GitLab CI. Designed as a fast baseline you can extend with deployment controls.
## Quick Start
```bash
# Detect stack
python3 scripts/stack_detector.py --repo . --format json > stack.json
# Generate GitHub Actions workflow
python3 scripts/pipeline_generator.py \
--input stack.json \
--platform github \
--output .github/workflows/ci.yml \
--format text
```
## Included Tools
- `scripts/stack_detector.py`: repository signal detection with JSON/text output
- `scripts/pipeline_generator.py`: generate GitHub/GitLab CI YAML from detection payload
## References
- `references/github-actions-templates.md`
- `references/gitlab-ci-templates.md`
- `references/deployment-gates.md`
## Installation
### Claude Code
```bash
cp -R engineering/ci-cd-pipeline-builder ~/.claude/skills/ci-cd-pipeline-builder
```
### OpenAI Codex
```bash
cp -R engineering/ci-cd-pipeline-builder ~/.codex/skills/ci-cd-pipeline-builder
```
### OpenClaw
```bash
cp -R engineering/ci-cd-pipeline-builder ~/.openclaw/skills/ci-cd-pipeline-builder
```
FILE:references/deployment-gates.md
# Deployment Gates
## Minimum Gate Policy
- `lint` must pass before `test`.
- `test` must pass before `build`.
- `build` artifact required for deploy jobs.
- Production deploy requires manual approval and protected branch.
## Environment Pattern
- `develop` -> auto deploy to staging
- `main` -> manual promote to production
## Rollback Requirement
Every deploy job should define a rollback command or procedure reference.
FILE:references/github-actions-templates.md
# GitHub Actions Templates
## Node.js Baseline
```yaml
name: Node CI
on: [push, pull_request]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
```
## Python Baseline
```yaml
name: Python CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: python3 -m pip install -U pip
- run: python3 -m pip install -r requirements.txt
- run: python3 -m pytest
```
FILE:references/gitlab-ci-templates.md
# GitLab CI Templates
## Node.js Baseline
```yaml
stages:
- lint
- test
- build
node_lint:
image: node:20
stage: lint
script:
- npm ci
- npm run lint
node_test:
image: node:20
stage: test
script:
- npm ci
- npm test
```
## Python Baseline
```yaml
stages:
- test
python_test:
image: python:3.12
stage: test
script:
- python3 -m pip install -U pip
- python3 -m pip install -r requirements.txt
- python3 -m pytest
```
FILE:scripts/pipeline_generator.py
#!/usr/bin/env python3
"""Generate CI pipeline YAML from detected stack data.
Input sources:
- --input stack report JSON file
- stdin stack report JSON
- --repo path (auto-detect stack)
Output:
- text/json summary
- pipeline YAML written via --output or printed to stdout
"""
import argparse
import json
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Any, Dict, List, Optional
class CLIError(Exception):
"""Raised for expected CLI failures."""
@dataclass
class PipelineSummary:
platform: str
output: str
stages: List[str]
uses_cache: bool
languages: List[str]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate CI/CD pipeline YAML from detected stack.")
parser.add_argument("--input", help="Stack report JSON file. If omitted, can read stdin JSON.")
parser.add_argument("--repo", help="Repository path for auto-detection fallback.")
parser.add_argument("--platform", choices=["github", "gitlab"], required=True, help="Target CI platform.")
parser.add_argument("--output", help="Write YAML to this file; otherwise print to stdout.")
parser.add_argument("--format", choices=["text", "json"], default="text", help="Summary output format.")
return parser.parse_args()
def load_json_input(input_path: Optional[str]) -> Optional[Dict[str, Any]]:
if input_path:
try:
return json.loads(Path(input_path).read_text(encoding="utf-8"))
except Exception as exc:
raise CLIError(f"Failed reading --input: {exc}") from exc
if not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if raw:
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
raise CLIError(f"Invalid JSON from stdin: {exc}") from exc
return None
def detect_stack(repo: Path) -> Dict[str, Any]:
scripts = {}
pkg_file = repo / "package.json"
if pkg_file.exists():
try:
pkg = json.loads(pkg_file.read_text(encoding="utf-8"))
raw_scripts = pkg.get("scripts", {})
if isinstance(raw_scripts, dict):
scripts = raw_scripts
except Exception:
scripts = {}
languages: List[str] = []
if pkg_file.exists():
languages.append("node")
if (repo / "pyproject.toml").exists() or (repo / "requirements.txt").exists():
languages.append("python")
if (repo / "go.mod").exists():
languages.append("go")
return {
"languages": sorted(set(languages)),
"signals": {
"pnpm_lock": (repo / "pnpm-lock.yaml").exists(),
"yarn_lock": (repo / "yarn.lock").exists(),
"npm_lock": (repo / "package-lock.json").exists(),
"dockerfile": (repo / "Dockerfile").exists(),
},
"lint_commands": ["npm run lint"] if "lint" in scripts else [],
"test_commands": ["npm test"] if "test" in scripts else [],
"build_commands": ["npm run build"] if "build" in scripts else [],
}
def select_node_install(signals: Dict[str, Any]) -> str:
if signals.get("pnpm_lock"):
return "pnpm install --frozen-lockfile"
if signals.get("yarn_lock"):
return "yarn install --frozen-lockfile"
return "npm ci"
def github_yaml(stack: Dict[str, Any]) -> str:
langs = stack.get("languages", [])
signals = stack.get("signals", {})
lint_cmds = stack.get("lint_commands", []) or ["echo 'No lint command configured'"]
test_cmds = stack.get("test_commands", []) or ["echo 'No test command configured'"]
build_cmds = stack.get("build_commands", []) or ["echo 'No build command configured'"]
lines: List[str] = [
"name: CI",
"on:",
" push:",
" branches: [main, develop]",
" pull_request:",
" branches: [main, develop]",
"",
"jobs:",
]
if "node" in langs:
lines.extend(
[
" node-ci:",
" runs-on: ubuntu-latest",
" steps:",
" - uses: actions/checkout@v4",
" - uses: actions/setup-node@v4",
" with:",
" node-version: '20'",
" cache: 'npm'",
f" - run: {select_node_install(signals)}",
]
)
for cmd in lint_cmds + test_cmds + build_cmds:
lines.append(f" - run: {cmd}")
if "python" in langs:
lines.extend(
[
" python-ci:",
" runs-on: ubuntu-latest",
" steps:",
" - uses: actions/checkout@v4",
" - uses: actions/setup-python@v5",
" with:",
" python-version: '3.12'",
" - run: python3 -m pip install -U pip",
" - run: python3 -m pip install -r requirements.txt || true",
" - run: python3 -m pytest || true",
]
)
if "go" in langs:
lines.extend(
[
" go-ci:",
" runs-on: ubuntu-latest",
" steps:",
" - uses: actions/checkout@v4",
" - uses: actions/setup-go@v5",
" with:",
" go-version: '1.22'",
" - run: go test ./...",
" - run: go build ./...",
]
)
return "\n".join(lines) + "\n"
def gitlab_yaml(stack: Dict[str, Any]) -> str:
langs = stack.get("languages", [])
signals = stack.get("signals", {})
lint_cmds = stack.get("lint_commands", []) or ["echo 'No lint command configured'"]
test_cmds = stack.get("test_commands", []) or ["echo 'No test command configured'"]
build_cmds = stack.get("build_commands", []) or ["echo 'No build command configured'"]
lines: List[str] = [
"stages:",
" - lint",
" - test",
" - build",
"",
]
if "node" in langs:
install_cmd = select_node_install(signals)
lines.extend(
[
"node_lint:",
" image: node:20",
" stage: lint",
" script:",
f" - {install_cmd}",
]
)
for cmd in lint_cmds:
lines.append(f" - {cmd}")
lines.extend(
[
"",
"node_test:",
" image: node:20",
" stage: test",
" script:",
f" - {install_cmd}",
]
)
for cmd in test_cmds:
lines.append(f" - {cmd}")
lines.extend(
[
"",
"node_build:",
" image: node:20",
" stage: build",
" script:",
f" - {install_cmd}",
]
)
for cmd in build_cmds:
lines.append(f" - {cmd}")
if "python" in langs:
lines.extend(
[
"",
"python_test:",
" image: python:3.12",
" stage: test",
" script:",
" - python3 -m pip install -U pip",
" - python3 -m pip install -r requirements.txt || true",
" - python3 -m pytest || true",
]
)
if "go" in langs:
lines.extend(
[
"",
"go_test:",
" image: golang:1.22",
" stage: test",
" script:",
" - go test ./...",
" - go build ./...",
]
)
return "\n".join(lines) + "\n"
def main() -> int:
args = parse_args()
stack = load_json_input(args.input)
if stack is None:
if not args.repo:
raise CLIError("Provide stack input via --input/stdin or set --repo for auto-detection.")
repo = Path(args.repo).resolve()
if not repo.exists() or not repo.is_dir():
raise CLIError(f"Invalid repo path: {repo}")
stack = detect_stack(repo)
if args.platform == "github":
yaml_content = github_yaml(stack)
else:
yaml_content = gitlab_yaml(stack)
output_path = args.output or "stdout"
if args.output:
out = Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(yaml_content, encoding="utf-8")
else:
print(yaml_content, end="")
summary = PipelineSummary(
platform=args.platform,
output=output_path,
stages=["lint", "test", "build"],
uses_cache=True,
languages=stack.get("languages", []),
)
if args.format == "json":
print(json.dumps(asdict(summary), indent=2), file=sys.stderr if not args.output else sys.stdout)
else:
text = (
"Pipeline generated\n"
f"- platform: {summary.platform}\n"
f"- output: {summary.output}\n"
f"- stages: {', '.join(summary.stages)}\n"
f"- languages: {', '.join(summary.languages) if summary.languages else 'none'}"
)
print(text, file=sys.stderr if not args.output else sys.stdout)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except CLIError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(2)
FILE:scripts/stack_detector.py
#!/usr/bin/env python3
"""Detect project stack/tooling signals for CI/CD pipeline generation.
Input sources:
- repository scan via --repo
- JSON via --input file
- JSON via stdin
Output:
- text summary or JSON payload
"""
import argparse
import json
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, List, Optional
class CLIError(Exception):
"""Raised for expected CLI failures."""
@dataclass
class StackReport:
repo: str
languages: List[str]
package_managers: List[str]
ci_targets: List[str]
test_commands: List[str]
build_commands: List[str]
lint_commands: List[str]
signals: Dict[str, bool]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Detect stack/tooling from a repository.")
parser.add_argument("--input", help="JSON input file (precomputed signal payload).")
parser.add_argument("--repo", default=".", help="Repository path to scan.")
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
return parser.parse_args()
def load_payload(input_path: Optional[str]) -> Optional[dict]:
if input_path:
try:
return json.loads(Path(input_path).read_text(encoding="utf-8"))
except Exception as exc:
raise CLIError(f"Failed reading --input file: {exc}") from exc
if not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if raw:
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
raise CLIError(f"Invalid JSON from stdin: {exc}") from exc
return None
def read_package_scripts(repo: Path) -> Dict[str, str]:
pkg = repo / "package.json"
if not pkg.exists():
return {}
try:
data = json.loads(pkg.read_text(encoding="utf-8"))
except Exception:
return {}
scripts = data.get("scripts", {})
return scripts if isinstance(scripts, dict) else {}
def detect(repo: Path) -> StackReport:
signals = {
"package_json": (repo / "package.json").exists(),
"pnpm_lock": (repo / "pnpm-lock.yaml").exists(),
"yarn_lock": (repo / "yarn.lock").exists(),
"npm_lock": (repo / "package-lock.json").exists(),
"pyproject": (repo / "pyproject.toml").exists(),
"requirements": (repo / "requirements.txt").exists(),
"go_mod": (repo / "go.mod").exists(),
"dockerfile": (repo / "Dockerfile").exists(),
"vercel": (repo / "vercel.json").exists(),
"helm": (repo / "helm").exists() or (repo / "charts").exists(),
"k8s": (repo / "k8s").exists() or (repo / "kubernetes").exists(),
}
languages: List[str] = []
package_managers: List[str] = []
ci_targets: List[str] = ["github", "gitlab"]
if signals["package_json"]:
languages.append("node")
if signals["pnpm_lock"]:
package_managers.append("pnpm")
elif signals["yarn_lock"]:
package_managers.append("yarn")
else:
package_managers.append("npm")
if signals["pyproject"] or signals["requirements"]:
languages.append("python")
package_managers.append("pip")
if signals["go_mod"]:
languages.append("go")
scripts = read_package_scripts(repo)
lint_commands: List[str] = []
test_commands: List[str] = []
build_commands: List[str] = []
if "lint" in scripts:
lint_commands.append("npm run lint")
if "test" in scripts:
test_commands.append("npm test")
if "build" in scripts:
build_commands.append("npm run build")
if "python" in languages:
lint_commands.append("python3 -m ruff check .")
test_commands.append("python3 -m pytest")
if "go" in languages:
lint_commands.append("go vet ./...")
test_commands.append("go test ./...")
build_commands.append("go build ./...")
return StackReport(
repo=str(repo.resolve()),
languages=sorted(set(languages)),
package_managers=sorted(set(package_managers)),
ci_targets=ci_targets,
test_commands=sorted(set(test_commands)),
build_commands=sorted(set(build_commands)),
lint_commands=sorted(set(lint_commands)),
signals=signals,
)
def format_text(report: StackReport) -> str:
lines = [
"Detected stack",
f"- repo: {report.repo}",
f"- languages: {', '.join(report.languages) if report.languages else 'none'}",
f"- package managers: {', '.join(report.package_managers) if report.package_managers else 'none'}",
f"- lint commands: {', '.join(report.lint_commands) if report.lint_commands else 'none'}",
f"- test commands: {', '.join(report.test_commands) if report.test_commands else 'none'}",
f"- build commands: {', '.join(report.build_commands) if report.build_commands else 'none'}",
]
return "\n".join(lines)
def main() -> int:
args = parse_args()
payload = load_payload(args.input)
if payload:
try:
report = StackReport(**payload)
except TypeError as exc:
raise CLIError(f"Invalid input payload for StackReport: {exc}") from exc
else:
repo = Path(args.repo).resolve()
if not repo.exists() or not repo.is_dir():
raise CLIError(f"Invalid repo path: {repo}")
report = detect(repo)
if args.format == "json":
print(json.dumps(asdict(report), indent=2))
else:
print(format_text(report))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except CLIError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(2)
Changelog Generator
---
name: "changelog-generator"
description: "Changelog Generator"
---
# Changelog Generator
**Tier:** POWERFUL
**Category:** Engineering
**Domain:** Release Management / Documentation
## Overview
Use this skill to produce consistent, auditable release notes from Conventional Commits. It separates commit parsing, semantic bump logic, and changelog rendering so teams can automate releases without losing editorial control.
## Core Capabilities
- Parse commit messages using Conventional Commit rules
- Detect semantic bump (`major`, `minor`, `patch`) from commit stream
- Render Keep a Changelog sections (`Added`, `Changed`, `Fixed`, etc.)
- Generate release entries from git ranges or provided commit input
- Enforce commit format with a dedicated linter script
- Support CI integration via machine-readable JSON output
## When to Use
- Before publishing a release tag
- During CI to generate release notes automatically
- During PR checks to block invalid commit message formats
- In monorepos where package changelogs require scoped filtering
- When converting raw git history into user-facing notes
## Key Workflows
### 1. Generate Changelog Entry From Git
```bash
python3 scripts/generate_changelog.py \
--from-tag v1.3.0 \
--to-tag v1.4.0 \
--next-version v1.4.0 \
--format markdown
```
### 2. Generate Entry From stdin/File Input
```bash
git log v1.3.0..v1.4.0 --pretty=format:'%s' | \
python3 scripts/generate_changelog.py --next-version v1.4.0 --format markdown
python3 scripts/generate_changelog.py --input commits.txt --next-version v1.4.0 --format json
```
### 3. Update `CHANGELOG.md`
```bash
python3 scripts/generate_changelog.py \
--from-tag v1.3.0 \
--to-tag HEAD \
--next-version v1.4.0 \
--write CHANGELOG.md
```
### 4. Lint Commits Before Merge
```bash
python3 scripts/commit_linter.py --from-ref origin/main --to-ref HEAD --strict --format text
```
Or file/stdin:
```bash
python3 scripts/commit_linter.py --input commits.txt --strict
cat commits.txt | python3 scripts/commit_linter.py --format json
```
## Conventional Commit Rules
Supported types:
- `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `build`, `ci`, `chore`
- `security`, `deprecated`, `remove`
Breaking changes:
- `type(scope)!: summary`
- Footer/body includes `BREAKING CHANGE:`
SemVer mapping:
- breaking -> `major`
- non-breaking `feat` -> `minor`
- all others -> `patch`
## Script Interfaces
- `python3 scripts/generate_changelog.py --help`
- Reads commits from git or stdin/`--input`
- Renders markdown or JSON
- Optional in-place changelog prepend
- `python3 scripts/commit_linter.py --help`
- Validates commit format
- Returns non-zero in `--strict` mode on violations
## Common Pitfalls
1. Mixing merge commit messages with release commit parsing
2. Using vague commit summaries that cannot become release notes
3. Failing to include migration guidance for breaking changes
4. Treating docs/chore changes as user-facing features
5. Overwriting historical changelog sections instead of prepending
## Best Practices
1. Keep commits small and intent-driven.
2. Scope commit messages (`feat(api): ...`) in multi-package repos.
3. Enforce linter checks in PR pipelines.
4. Review generated markdown before publishing.
5. Tag releases only after changelog generation succeeds.
6. Keep an `[Unreleased]` section for manual curation when needed.
## References
- [references/ci-integration.md](references/ci-integration.md)
- [references/changelog-formatting-guide.md](references/changelog-formatting-guide.md)
- [references/monorepo-strategy.md](references/monorepo-strategy.md)
- [README.md](README.md)
## Release Governance
Use this release flow for predictability:
1. Lint commit history for target release range.
2. Generate changelog draft from commits.
3. Manually adjust wording for customer clarity.
4. Validate semver bump recommendation.
5. Tag release only after changelog is approved.
## Output Quality Checks
- Each bullet is user-meaningful, not implementation noise.
- Breaking changes include migration action.
- Security fixes are isolated in `Security` section.
- Sections with no entries are omitted.
- Duplicate bullets across sections are removed.
## CI Policy
- Run `commit_linter.py --strict` on all PRs.
- Block merge on invalid conventional commits.
- Auto-generate draft release notes on tag push.
- Require human approval before writing into `CHANGELOG.md` on main branch.
## Monorepo Guidance
- Prefer commit scopes aligned to package names.
- Filter commit stream by scope for package-specific releases.
- Keep infra-wide changes in root changelog.
- Store package changelogs near package roots for ownership clarity.
## Failure Handling
- If no valid conventional commits found: fail early, do not generate misleading empty notes.
- If git range invalid: surface explicit range in error output.
- If write target missing: create safe changelog header scaffolding.
FILE:README.md
# Changelog Generator
Automates release notes from Conventional Commits with Keep a Changelog output and strict commit linting. Designed for CI-friendly release workflows.
## Quick Start
```bash
# Generate entry from git range
python3 scripts/generate_changelog.py \
--from-tag v1.2.0 \
--to-tag v1.3.0 \
--next-version v1.3.0 \
--format markdown
# Lint commit subjects
python3 scripts/commit_linter.py --from-ref origin/main --to-ref HEAD --strict --format text
```
## Included Tools
- `scripts/generate_changelog.py`: parse commits, infer semver bump, render markdown/JSON, optional file prepend
- `scripts/commit_linter.py`: validate commit subjects against Conventional Commits rules
## References
- `references/ci-integration.md`
- `references/changelog-formatting-guide.md`
- `references/monorepo-strategy.md`
## Installation
### Claude Code
```bash
cp -R engineering/changelog-generator ~/.claude/skills/changelog-generator
```
### OpenAI Codex
```bash
cp -R engineering/changelog-generator ~/.codex/skills/changelog-generator
```
### OpenClaw
```bash
cp -R engineering/changelog-generator ~/.openclaw/skills/changelog-generator
```
FILE:references/changelog-formatting-guide.md
# Changelog Formatting Guide
Use Keep a Changelog section ordering:
1. Security
2. Added
3. Changed
4. Deprecated
5. Removed
6. Fixed
Rules:
- One bullet = one user-visible change.
- Lead with impact, not implementation detail.
- Keep bullets short and actionable.
- Include migration note for breaking changes.
FILE:references/ci-integration.md
# CI Integration Examples
## GitHub Actions
```yaml
name: Changelog Check
on: [pull_request]
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 engineering/changelog-generator/scripts/commit_linter.py \
--from-ref origin/main --to-ref HEAD --strict
```
## GitLab CI
```yaml
changelog_lint:
image: python:3.12
stage: test
script:
- python3 engineering/changelog-generator/scripts/commit_linter.py --to-ref HEAD --strict
```
FILE:references/monorepo-strategy.md
# Monorepo Changelog Strategy
## Approaches
| Strategy | When to use | Tradeoff |
|----------|-------------|----------|
| Single root changelog | Product-wide releases, small teams | Simple but loses package-level detail |
| Per-package changelogs | Independent versioning, large teams | Clear ownership but harder to see full picture |
| Hybrid model | Root summary + package-specific details | Best of both, more maintenance |
## Commit Scoping Pattern
Enforce scoped conventional commits to enable per-package filtering:
```
feat(payments): add Stripe webhook handler
fix(auth): handle expired refresh tokens
chore(infra): bump base Docker image
```
**Rules:**
- Scope must match a package/directory name exactly
- Unscoped commits go to root changelog only
- Multi-package changes get separate scoped commits (not one mega-commit)
## Filtering for Package Releases
```bash
# Generate changelog for 'payments' package only
git log v1.3.0..HEAD --pretty=format:'%s' | grep '^[a-z]*\(payments\)' | \
python3 scripts/generate_changelog.py --next-version v1.4.0 --format markdown
```
## Ownership Model
- Package maintainers own their scoped changelog
- Platform/infra team owns root changelog
- CI enforces scope presence on all commits touching package directories
- Root changelog aggregates breaking changes from all packages for visibility
FILE:scripts/commit_linter.py
#!/usr/bin/env python3
"""Lint commit messages against Conventional Commits.
Input sources (priority order):
1) --input file (one commit subject per line)
2) stdin lines
3) git range via --from-ref/--to-ref
Use --strict for non-zero exit on violations.
"""
import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import List, Optional
CONVENTIONAL_RE = re.compile(
r"^(feat|fix|perf|refactor|docs|test|build|ci|chore|security|deprecated|remove)"
r"(\([a-z0-9._/-]+\))?(!)?:\s+.{1,120}$"
)
class CLIError(Exception):
"""Raised for expected CLI errors."""
@dataclass
class LintReport:
total: int
valid: int
invalid: int
violations: List[str]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Validate conventional commit subjects.")
parser.add_argument("--input", help="File with commit subjects (one per line).")
parser.add_argument("--from-ref", help="Git ref start (exclusive).")
parser.add_argument("--to-ref", help="Git ref end (inclusive).")
parser.add_argument("--strict", action="store_true", help="Exit non-zero when violations exist.")
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
return parser.parse_args()
def lines_from_file(path: str) -> List[str]:
try:
return [line.strip() for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()]
except Exception as exc:
raise CLIError(f"Failed reading --input file: {exc}") from exc
def lines_from_stdin() -> List[str]:
if sys.stdin.isatty():
return []
data = sys.stdin.read()
return [line.strip() for line in data.splitlines() if line.strip()]
def lines_from_git(args: argparse.Namespace) -> List[str]:
if not args.to_ref:
return []
range_spec = f"{args.from_ref}..{args.to_ref}" if args.from_ref else args.to_ref
try:
proc = subprocess.run(
["git", "log", range_spec, "--pretty=format:%s", "--no-merges"],
text=True,
capture_output=True,
check=True,
)
except subprocess.CalledProcessError as exc:
raise CLIError(f"git log failed for range '{range_spec}': {exc.stderr.strip()}") from exc
return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
def load_lines(args: argparse.Namespace) -> List[str]:
if args.input:
return lines_from_file(args.input)
stdin_lines = lines_from_stdin()
if stdin_lines:
return stdin_lines
git_lines = lines_from_git(args)
if git_lines:
return git_lines
raise CLIError("No commit input found. Use --input, stdin, or --to-ref.")
def lint(lines: List[str]) -> LintReport:
violations: List[str] = []
valid = 0
for idx, line in enumerate(lines, start=1):
if CONVENTIONAL_RE.match(line):
valid += 1
continue
violations.append(f"line {idx}: {line}")
return LintReport(total=len(lines), valid=valid, invalid=len(violations), violations=violations)
def format_text(report: LintReport) -> str:
lines = [
"Conventional commit lint report",
f"- total: {report.total}",
f"- valid: {report.valid}",
f"- invalid: {report.invalid}",
]
if report.violations:
lines.append("Violations:")
lines.extend([f"- {v}" for v in report.violations])
return "\n".join(lines)
def main() -> int:
args = parse_args()
lines = load_lines(args)
report = lint(lines)
if args.format == "json":
print(json.dumps(asdict(report), indent=2))
else:
print(format_text(report))
if args.strict and report.invalid > 0:
return 1
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except CLIError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(2)
FILE:scripts/generate_changelog.py
#!/usr/bin/env python3
"""Generate changelog entries from Conventional Commits.
Input sources (priority order):
1) --input file with one commit subject per line
2) stdin commit subjects
3) git log from --from-tag/--to-tag or --from-ref/--to-ref
Outputs markdown or JSON and can prepend into CHANGELOG.md.
"""
import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass, asdict, field
from datetime import date
from pathlib import Path
from typing import Dict, List, Optional
COMMIT_RE = re.compile(
r"^(?P<type>feat|fix|perf|refactor|docs|test|build|ci|chore|security|deprecated|remove)"
r"(?:\((?P<scope>[^)]+)\))?(?P<breaking>!)?:\s+(?P<summary>.+)$"
)
SECTION_MAP = {
"feat": "Added",
"fix": "Fixed",
"perf": "Changed",
"refactor": "Changed",
"security": "Security",
"deprecated": "Deprecated",
"remove": "Removed",
}
class CLIError(Exception):
"""Raised for expected CLI failures."""
@dataclass
class ParsedCommit:
raw: str
ctype: str
scope: Optional[str]
summary: str
breaking: bool
@dataclass
class ChangelogEntry:
version: str
release_date: str
sections: Dict[str, List[str]] = field(default_factory=dict)
breaking_changes: List[str] = field(default_factory=list)
bump: str = "patch"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate changelog from conventional commits.")
parser.add_argument("--input", help="Text file with one commit subject per line.")
parser.add_argument("--from-tag", help="Git tag start (exclusive).")
parser.add_argument("--to-tag", help="Git tag end (inclusive).")
parser.add_argument("--from-ref", help="Git ref start (exclusive).")
parser.add_argument("--to-ref", help="Git ref end (inclusive).")
parser.add_argument("--next-version", default="Unreleased", help="Version label for the generated entry.")
parser.add_argument("--date", dest="entry_date", default=str(date.today()), help="Release date (YYYY-MM-DD).")
parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Output format.")
parser.add_argument("--write", help="Prepend generated markdown entry into this changelog file.")
return parser.parse_args()
def read_lines_from_file(path: str) -> List[str]:
try:
return [line.strip() for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()]
except Exception as exc:
raise CLIError(f"Failed reading --input file: {exc}") from exc
def read_lines_from_stdin() -> List[str]:
if sys.stdin.isatty():
return []
payload = sys.stdin.read()
return [line.strip() for line in payload.splitlines() if line.strip()]
def read_lines_from_git(args: argparse.Namespace) -> List[str]:
if args.from_tag or args.to_tag:
if not args.to_tag:
raise CLIError("--to-tag is required when using tag range.")
start = args.from_tag
end = args.to_tag
elif args.from_ref or args.to_ref:
if not args.to_ref:
raise CLIError("--to-ref is required when using ref range.")
start = args.from_ref
end = args.to_ref
else:
return []
range_spec = f"{start}..{end}" if start else end
try:
proc = subprocess.run(
["git", "log", range_spec, "--pretty=format:%s", "--no-merges"],
text=True,
capture_output=True,
check=True,
)
except subprocess.CalledProcessError as exc:
raise CLIError(f"git log failed for range '{range_spec}': {exc.stderr.strip()}") from exc
return [line.strip() for line in proc.stdout.splitlines() if line.strip()]
def load_commits(args: argparse.Namespace) -> List[str]:
if args.input:
return read_lines_from_file(args.input)
stdin_lines = read_lines_from_stdin()
if stdin_lines:
return stdin_lines
git_lines = read_lines_from_git(args)
if git_lines:
return git_lines
raise CLIError("No commit input found. Use --input, stdin, or git range flags.")
def parse_commits(lines: List[str]) -> List[ParsedCommit]:
parsed: List[ParsedCommit] = []
for line in lines:
match = COMMIT_RE.match(line)
if not match:
continue
ctype = match.group("type")
scope = match.group("scope")
summary = match.group("summary")
breaking = bool(match.group("breaking")) or "BREAKING CHANGE" in line
parsed.append(ParsedCommit(raw=line, ctype=ctype, scope=scope, summary=summary, breaking=breaking))
return parsed
def determine_bump(commits: List[ParsedCommit]) -> str:
if any(c.breaking for c in commits):
return "major"
if any(c.ctype == "feat" for c in commits):
return "minor"
return "patch"
def build_entry(commits: List[ParsedCommit], version: str, entry_date: str) -> ChangelogEntry:
sections: Dict[str, List[str]] = {
"Security": [],
"Added": [],
"Changed": [],
"Deprecated": [],
"Removed": [],
"Fixed": [],
}
breaking_changes: List[str] = []
for commit in commits:
if commit.breaking:
breaking_changes.append(commit.summary)
section = SECTION_MAP.get(commit.ctype)
if section:
line = commit.summary if not commit.scope else f"{commit.scope}: {commit.summary}"
sections[section].append(line)
sections = {k: v for k, v in sections.items() if v}
return ChangelogEntry(
version=version,
release_date=entry_date,
sections=sections,
breaking_changes=breaking_changes,
bump=determine_bump(commits),
)
def render_markdown(entry: ChangelogEntry) -> str:
lines = [f"## [{entry.version}] - {entry.release_date}", ""]
if entry.breaking_changes:
lines.append("### Breaking")
lines.extend([f"- {item}" for item in entry.breaking_changes])
lines.append("")
ordered_sections = ["Security", "Added", "Changed", "Deprecated", "Removed", "Fixed"]
for section in ordered_sections:
items = entry.sections.get(section, [])
if not items:
continue
lines.append(f"### {section}")
lines.extend([f"- {item}" for item in items])
lines.append("")
lines.append(f"<!-- recommended-semver-bump: {entry.bump} -->")
return "\n".join(lines).strip() + "\n"
def prepend_changelog(path: Path, entry_md: str) -> None:
if path.exists():
original = path.read_text(encoding="utf-8")
else:
original = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n"
if original.startswith("# Changelog"):
first_break = original.find("\n")
head = original[: first_break + 1]
tail = original[first_break + 1 :].lstrip("\n")
combined = f"{head}\n{entry_md}\n{tail}"
else:
combined = f"# Changelog\n\n{entry_md}\n{original}"
path.write_text(combined, encoding="utf-8")
def main() -> int:
args = parse_args()
lines = load_commits(args)
parsed = parse_commits(lines)
if not parsed:
raise CLIError("No valid conventional commit messages found in input.")
entry = build_entry(parsed, args.next_version, args.entry_date)
if args.format == "json":
print(json.dumps(asdict(entry), indent=2))
else:
markdown = render_markdown(entry)
print(markdown, end="")
if args.write:
prepend_changelog(Path(args.write), markdown)
if args.format == "json" and args.write:
prepend_changelog(Path(args.write), render_markdown(entry))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except CLIError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(2)
API Test Suite Builder
---
name: "api-test-suite-builder"
description: "API Test Suite Builder"
---
# API Test Suite Builder
**Tier:** POWERFUL
**Category:** Engineering
**Domain:** Testing / API Quality
---
## Overview
Scans API route definitions across frameworks (Next.js App Router, Express, FastAPI, Django REST) and
auto-generates comprehensive test suites covering auth, input validation, error codes, pagination, file
uploads, and rate limiting. Outputs ready-to-run test files for Vitest+Supertest (Node) or Pytest+httpx
(Python).
---
## Core Capabilities
- **Route detection** — scan source files to extract all API endpoints
- **Auth coverage** — valid/invalid/expired tokens, missing auth header
- **Input validation** — missing fields, wrong types, boundary values, injection attempts
- **Error code matrix** — 400/401/403/404/422/500 for each route
- **Pagination** — first/last/empty/oversized pages
- **File uploads** — valid, oversized, wrong MIME type, empty
- **Rate limiting** — burst detection, per-user vs global limits
---
## When to Use
- New API added — generate test scaffold before writing implementation (TDD)
- Legacy API with no tests — scan and generate baseline coverage
- API contract review — verify existing tests match current route definitions
- Pre-release regression check — ensure all routes have at least smoke tests
- Security audit prep — generate adversarial input tests
---
## Route Detection
### Next.js App Router
```bash
# Find all route handlers
find ./app/api -name "route.ts" -o -name "route.js" | sort
# Extract HTTP methods from each route file
grep -rn "export async function\|export function" app/api/**/route.ts | \
grep -oE "(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)" | sort -u
# Full route map
find ./app/api -name "route.ts" | while read f; do
route=$(echo $f | sed 's|./app||' | sed 's|/route.ts||')
methods=$(grep -oE "export (async )?function (GET|POST|PUT|PATCH|DELETE)" "$f" | \
grep -oE "(GET|POST|PUT|PATCH|DELETE)")
echo "$methods $route"
done
```
### Express
```bash
# Find all router files
find ./src -name "*.ts" -o -name "*.js" | xargs grep -l "router\.\(get\|post\|put\|delete\|patch\)" 2>/dev/null
# Extract routes with line numbers
grep -rn "router\.\(get\|post\|put\|delete\|patch\)\|app\.\(get\|post\|put\|delete\|patch\)" \
src/ --include="*.ts" | grep -oE "(get|post|put|delete|patch)\(['\"][^'\"]*['\"]"
# Generate route map
grep -rn "router\.\|app\." src/ --include="*.ts" | \
grep -oE "\.(get|post|put|delete|patch)\(['\"][^'\"]+['\"]" | \
sed "s/\.\(.*\)('\(.*\)'/\U\1 \2/"
```
### FastAPI
```bash
# Find all route decorators
grep -rn "@app\.\|@router\." . --include="*.py" | \
grep -E "@(app|router)\.(get|post|put|delete|patch)"
# Extract with path and function name
grep -rn "@\(app\|router\)\.\(get\|post\|put\|delete\|patch\)" . --include="*.py" | \
grep -oE "@(app|router)\.(get|post|put|delete|patch)\(['\"][^'\"]*['\"]"
```
### Django REST Framework
```bash
# urlpatterns extraction
grep -rn "path\|re_path\|url(" . --include="*.py" | grep "urlpatterns" -A 50 | \
grep -E "path\(['\"]" | grep -oE "['\"][^'\"]+['\"]" | head -40
# ViewSet router registration
grep -rn "router\.register\|DefaultRouter\|SimpleRouter" . --include="*.py"
```
---
## Test Generation Patterns
### Auth Test Matrix
For every authenticated endpoint, generate:
| Test Case | Expected Status |
|-----------|----------------|
| No Authorization header | 401 |
| Invalid token format | 401 |
| Valid token, wrong user role | 403 |
| Expired JWT token | 401 |
| Valid token, correct role | 2xx |
| Token from deleted user | 401 |
### Input Validation Matrix
For every POST/PUT/PATCH endpoint with a request body:
| Test Case | Expected Status |
|-----------|----------------|
| Empty body `{}` | 400 or 422 |
| Missing required fields (one at a time) | 400 or 422 |
| Wrong type (string where int expected) | 400 or 422 |
| Boundary: value at min-1 | 400 or 422 |
| Boundary: value at min | 2xx |
| Boundary: value at max | 2xx |
| Boundary: value at max+1 | 400 or 422 |
| SQL injection in string field | 400 or 200 (sanitized) |
| XSS payload in string field | 400 or 200 (sanitized) |
| Null values for required fields | 400 or 422 |
---
## Example Test Files
→ See references/example-test-files.md for details
## Generating Tests from Route Scan
When given a codebase, follow this process:
1. **Scan routes** using the detection commands above
2. **Read each route handler** to understand:
- Expected request body schema
- Auth requirements (middleware, decorators)
- Return types and status codes
- Business rules (ownership, role checks)
3. **Generate test file** per route group using the patterns above
4. **Name tests descriptively**: `"returns 401 when token is expired"` not `"auth test 3"`
5. **Use factories/fixtures** for test data — never hardcode IDs
6. **Assert response shape**, not just status code
---
## Common Pitfalls
- **Testing only happy paths** — 80% of bugs live in error paths; test those first
- **Hardcoded test data IDs** — use factories/fixtures; IDs change between environments
- **Shared state between tests** — always clean up in afterEach/afterAll
- **Testing implementation, not behavior** — test what the API returns, not how it does it
- **Missing boundary tests** — off-by-one errors are extremely common in pagination and limits
- **Not testing token expiry** — expired tokens behave differently from invalid ones
- **Ignoring Content-Type** — test that API rejects wrong content types (xml when json expected)
---
## Best Practices
1. One describe block per endpoint — keeps failures isolated and readable
2. Seed minimal data — don't load the entire DB; create only what the test needs
3. Use `beforeAll` for shared setup, `afterAll` for cleanup — not `beforeEach` for expensive ops
4. Assert specific error messages/fields, not just status codes
5. Test that sensitive fields (password, secret) are never in responses
6. For auth tests, always test the "missing header" case separately from "invalid token"
7. Add rate limit tests last — they can interfere with other test suites if run in parallel
FILE:references/example-test-files.md
# api-test-suite-builder reference
## Example Test Files
### Example 1 — Node.js: Vitest + Supertest (Next.js API Route)
```typescript
// tests/api/users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import request from 'supertest'
import { createServer } from '@/test/helpers/server'
import { generateJWT, generateExpiredJWT } from '@/test/helpers/auth'
import { createTestUser, cleanupTestUsers } from '@/test/helpers/db'
const app = createServer()
describe('GET /api/users/:id', () => {
let validToken: string
let adminToken: string
let testUserId: string
beforeAll(async () => {
const user = await createTestUser({ role: 'user' })
const admin = await createTestUser({ role: 'admin' })
testUserId = user.id
validToken = generateJWT(user)
adminToken = generateJWT(admin)
})
afterAll(async () => {
await cleanupTestUsers()
})
// --- Auth tests ---
it('returns 401 with no auth header', async () => {
const res = await request(app).get(`/api/users/testUserId`)
expect(res.status).toBe(401)
expect(res.body).toHaveProperty('error')
})
it('returns 401 with malformed token', async () => {
const res = await request(app)
.get(`/api/users/testUserId`)
.set('Authorization', 'Bearer not-a-real-jwt')
expect(res.status).toBe(401)
})
it('returns 401 with expired token', async () => {
const expiredToken = generateExpiredJWT({ id: testUserId })
const res = await request(app)
.get(`/api/users/testUserId`)
.set('Authorization', `Bearer expiredToken`)
expect(res.status).toBe(401)
expect(res.body.error).toMatch(/expired/i)
})
it('returns 403 when accessing another user\'s profile without admin', async () => {
const otherUser = await createTestUser({ role: 'user' })
const otherToken = generateJWT(otherUser)
const res = await request(app)
.get(`/api/users/testUserId`)
.set('Authorization', `Bearer otherToken`)
expect(res.status).toBe(403)
await cleanupTestUsers([otherUser.id])
})
it('returns 200 with valid token for own profile', async () => {
const res = await request(app)
.get(`/api/users/testUserId`)
.set('Authorization', `Bearer validToken`)
expect(res.status).toBe(200)
expect(res.body).toMatchObject({ id: testUserId })
expect(res.body).not.toHaveProperty('password')
expect(res.body).not.toHaveProperty('hashedPassword')
})
it('returns 404 for non-existent user', async () => {
const res = await request(app)
.get('/api/users/00000000-0000-0000-0000-000000000000')
.set('Authorization', `Bearer adminToken`)
expect(res.status).toBe(404)
})
// --- Input validation ---
it('returns 400 for invalid UUID format', async () => {
const res = await request(app)
.get('/api/users/not-a-uuid')
.set('Authorization', `Bearer adminToken`)
expect(res.status).toBe(400)
})
})
describe('POST /api/users', () => {
let adminToken: string
beforeAll(async () => {
const admin = await createTestUser({ role: 'admin' })
adminToken = generateJWT(admin)
})
afterAll(cleanupTestUsers)
// --- Input validation ---
it('returns 422 when body is empty', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer adminToken`)
.send({})
expect(res.status).toBe(422)
expect(res.body.errors).toBeDefined()
})
it('returns 422 when email is missing', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer adminToken`)
.send({ name: "test-user", role: 'user' })
expect(res.status).toBe(422)
expect(res.body.errors).toContainEqual(
expect.objectContaining({ field: 'email' })
)
})
it('returns 422 for invalid email format', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer adminToken`)
.send({ email: 'not-an-email', name: "test", role: 'user' })
expect(res.status).toBe(422)
})
it('returns 422 for SQL injection attempt in email field', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer adminToken`)
.send({ email: "' OR '1'='1", name: "hacker", role: 'user' })
expect(res.status).toBe(422)
})
it('returns 409 when email already exists', async () => {
const existing = await createTestUser({ role: 'user' })
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer adminToken`)
.send({ email: existing.email, name: "duplicate", role: 'user' })
expect(res.status).toBe(409)
})
it('creates user successfully with valid data', async () => {
const res = await request(app)
.post('/api/users')
.set('Authorization', `Bearer adminToken`)
.send({ email: '[email protected]', name: "new-user", role: 'user' })
expect(res.status).toBe(201)
expect(res.body).toHaveProperty('id')
expect(res.body.email).toBe('[email protected]')
expect(res.body).not.toHaveProperty('password')
})
})
describe('GET /api/users (pagination)', () => {
let adminToken: string
beforeAll(async () => {
const admin = await createTestUser({ role: 'admin' })
adminToken = generateJWT(admin)
// Create 15 test users for pagination
await Promise.all(Array.from({ length: 15 }, (_, i) =>
createTestUser({ email: `pagtesti@example.com` })
))
})
afterAll(cleanupTestUsers)
it('returns first page with default limit', async () => {
const res = await request(app)
.get('/api/users')
.set('Authorization', `Bearer adminToken`)
expect(res.status).toBe(200)
expect(res.body.data).toBeInstanceOf(Array)
expect(res.body).toHaveProperty('total')
expect(res.body).toHaveProperty('page')
expect(res.body).toHaveProperty('pageSize')
})
it('returns empty array for page beyond total', async () => {
const res = await request(app)
.get('/api/users?page=9999')
.set('Authorization', `Bearer adminToken`)
expect(res.status).toBe(200)
expect(res.body.data).toHaveLength(0)
})
it('returns 400 for negative page number', async () => {
const res = await request(app)
.get('/api/users?page=-1')
.set('Authorization', `Bearer adminToken`)
expect(res.status).toBe(400)
})
it('caps pageSize at maximum allowed value', async () => {
const res = await request(app)
.get('/api/users?pageSize=9999')
.set('Authorization', `Bearer adminToken`)
expect(res.status).toBe(200)
expect(res.body.data.length).toBeLessThanOrEqual(100)
})
})
```
---
### Example 2 — Node.js: File Upload Tests
```typescript
// tests/api/uploads.test.ts
import { describe, it, expect } from 'vitest'
import request from 'supertest'
import path from 'path'
import fs from 'fs'
import { createServer } from '@/test/helpers/server'
import { generateJWT } from '@/test/helpers/auth'
import { createTestUser } from '@/test/helpers/db'
const app = createServer()
describe('POST /api/upload', () => {
let validToken: string
beforeAll(async () => {
const user = await createTestUser({ role: 'user' })
validToken = generateJWT(user)
})
it('returns 401 without authentication', async () => {
const res = await request(app)
.post('/api/upload')
.attach('file', Buffer.from('test'), 'test.pdf')
expect(res.status).toBe(401)
})
it('returns 400 when no file attached', async () => {
const res = await request(app)
.post('/api/upload')
.set('Authorization', `Bearer validToken`)
expect(res.status).toBe(400)
expect(res.body.error).toMatch(/file/i)
})
it('returns 400 for unsupported file type (exe)', async () => {
const res = await request(app)
.post('/api/upload')
.set('Authorization', `Bearer validToken`)
.attach('file', Buffer.from('MZ fake exe'), { filename: "virusexe", contentType: 'application/octet-stream' })
expect(res.status).toBe(400)
expect(res.body.error).toMatch(/type|format|allowed/i)
})
it('returns 413 for oversized file (>10MB)', async () => {
const largeBuf = Buffer.alloc(11 * 1024 * 1024) // 11MB
const res = await request(app)
.post('/api/upload')
.set('Authorization', `Bearer validToken`)
.attach('file', largeBuf, { filename: "largepdf", contentType: 'application/pdf' })
expect(res.status).toBe(413)
})
it('returns 400 for empty file (0 bytes)', async () => {
const res = await request(app)
.post('/api/upload')
.set('Authorization', `Bearer validToken`)
.attach('file', Buffer.alloc(0), { filename: "emptypdf", contentType: 'application/pdf' })
expect(res.status).toBe(400)
})
it('rejects MIME type spoofing (pdf extension but exe content)', async () => {
// Real malicious file: exe magic bytes but pdf extension
const fakeExe = Buffer.from('4D5A9000', 'hex') // MZ header
const res = await request(app)
.post('/api/upload')
.set('Authorization', `Bearer validToken`)
.attach('file', fakeExe, { filename: "documentpdf", contentType: 'application/pdf' })
// Should detect magic bytes mismatch
expect([400, 415]).toContain(res.status)
})
it('accepts valid PDF file', async () => {
const pdfHeader = Buffer.from('%PDF-1.4 test content')
const res = await request(app)
.post('/api/upload')
.set('Authorization', `Bearer validToken`)
.attach('file', pdfHeader, { filename: "validpdf", contentType: 'application/pdf' })
expect(res.status).toBe(200)
expect(res.body).toHaveProperty('url')
expect(res.body).toHaveProperty('id')
})
})
```
---
### Example 3 — Python: Pytest + httpx (FastAPI)
```python
# tests/api/test_items.py
import pytest
import httpx
from datetime import datetime, timedelta
import jwt
BASE_URL = "http://localhost:8000"
JWT_SECRET = "test-secret" # use test config, never production secret
def make_token(user_id: str, role: str = "user", expired: bool = False) -> str:
exp = datetime.utcnow() + (timedelta(hours=-1) if expired else timedelta(hours=1))
return jwt.encode(
{"sub": user_id, "role": role, "exp": exp},
JWT_SECRET,
algorithm="HS256",
)
@pytest.fixture
def client():
with httpx.Client(base_url=BASE_URL) as c:
yield c
@pytest.fixture
def valid_token():
return make_token("user-123", role="user")
@pytest.fixture
def admin_token():
return make_token("admin-456", role="admin")
@pytest.fixture
def expired_token():
return make_token("user-123", expired=True)
class TestGetItem:
def test_returns_401_without_auth(self, client):
res = client.get("/api/items/1")
assert res.status_code == 401
def test_returns_401_with_invalid_token(self, client):
res = client.get("/api/items/1", headers={"Authorization": "Bearer garbage"})
assert res.status_code == 401
def test_returns_401_with_expired_token(self, client, expired_token):
res = client.get("/api/items/1", headers={"Authorization": f"Bearer {expired_token}"})
assert res.status_code == 401
assert "expired" in res.json().get("detail", "").lower()
def test_returns_404_for_nonexistent_item(self, client, valid_token):
res = client.get(
"/api/items/99999999",
headers={"Authorization": f"Bearer {valid_token}"},
)
assert res.status_code == 404
def test_returns_400_for_invalid_id_format(self, client, valid_token):
res = client.get(
"/api/items/not-a-number",
headers={"Authorization": f"Bearer {valid_token}"},
)
assert res.status_code in (400, 422)
def test_returns_200_with_valid_auth(self, client, valid_token, test_item):
res = client.get(
f"/api/items/{test_item['id']}",
headers={"Authorization": f"Bearer {valid_token}"},
)
assert res.status_code == 200
data = res.json()
assert data["id"] == test_item["id"]
assert "password" not in data
class TestCreateItem:
def test_returns_422_with_empty_body(self, client, admin_token):
res = client.post(
"/api/items",
json={},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert res.status_code == 422
errors = res.json()["detail"]
assert len(errors) > 0
def test_returns_422_with_missing_required_field(self, client, admin_token):
res = client.post(
"/api/items",
json={"description": "no name field"},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert res.status_code == 422
fields = [e["loc"][-1] for e in res.json()["detail"]]
assert "name" in fields
def test_returns_422_with_wrong_type(self, client, admin_token):
res = client.post(
"/api/items",
json={"name": "test", "price": "not-a-number"},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert res.status_code == 422
@pytest.mark.parametrize("price", [-1, -0.01])
def test_returns_422_for_negative_price(self, client, admin_token, price):
res = client.post(
"/api/items",
json={"name": "test", "price": price},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert res.status_code == 422
def test_returns_422_for_price_exceeding_max(self, client, admin_token):
res = client.post(
"/api/items",
json={"name": "test", "price": 1_000_001},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert res.status_code == 422
def test_creates_item_successfully(self, client, admin_token):
res = client.post(
"/api/items",
json={"name": "New Widget", "price": 9.99, "category": "tools"},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert res.status_code == 201
data = res.json()
assert "id" in data
assert data["name"] == "New Widget"
def test_returns_403_for_non_admin(self, client, valid_token):
res = client.post(
"/api/items",
json={"name": "test", "price": 1.0},
headers={"Authorization": f"Bearer {valid_token}"},
)
assert res.status_code == 403
class TestPagination:
def test_returns_paginated_response(self, client, valid_token):
res = client.get(
"/api/items?page=1&size=10",
headers={"Authorization": f"Bearer {valid_token}"},
)
assert res.status_code == 200
data = res.json()
assert "items" in data
assert "total" in data
assert "page" in data
assert len(data["items"]) <= 10
def test_empty_result_for_out_of_range_page(self, client, valid_token):
res = client.get(
"/api/items?page=99999",
headers={"Authorization": f"Bearer {valid_token}"},
)
assert res.status_code == 200
assert res.json()["items"] == []
def test_returns_422_for_page_zero(self, client, valid_token):
res = client.get(
"/api/items?page=0",
headers={"Authorization": f"Bearer {valid_token}"},
)
assert res.status_code == 422
def test_caps_page_size_at_maximum(self, client, valid_token):
res = client.get(
"/api/items?size=9999",
headers={"Authorization": f"Bearer {valid_token}"},
)
assert res.status_code == 200
assert len(res.json()["items"]) <= 100 # max page size
class TestRateLimiting:
def test_rate_limit_after_burst(self, client, valid_token):
responses = []
for _ in range(60): # exceed typical 50/min limit
res = client.get(
"/api/items",
headers={"Authorization": f"Bearer {valid_token}"},
)
responses.append(res.status_code)
if res.status_code == 429:
break
assert 429 in responses, "Rate limit was not triggered"
def test_rate_limit_response_has_retry_after(self, client, valid_token):
for _ in range(60):
res = client.get("/api/items", headers={"Authorization": f"Bearer {valid_token}"})
if res.status_code == 429:
assert "Retry-After" in res.headers or "retry_after" in res.json()
break
```
---
Agent Workflow Designer
---
name: "agent-workflow-designer"
description: "Agent Workflow Designer"
---
# Agent Workflow Designer
**Tier:** POWERFUL
**Category:** Engineering
**Domain:** Multi-Agent Systems / AI Orchestration
---
## Overview
Design production-grade multi-agent orchestration systems. Covers five core patterns (sequential pipeline, parallel fan-out/fan-in, hierarchical delegation, event-driven, consensus), platform-specific implementations, handoff protocols, state management, error recovery, context window budgeting, and cost optimization.
---
## Core Capabilities
- Pattern selection guide for any orchestration requirement
- Handoff protocol templates (structured context passing)
- State management patterns for multi-agent workflows
- Error recovery and retry strategies
- Context window budget management
- Cost optimization strategies per platform
- Platform-specific configs: Claude Code Agent Teams, OpenClaw, CrewAI, AutoGen
---
## When to Use
- Building a multi-step AI pipeline that exceeds one agent's context capacity
- Parallelizing research, generation, or analysis tasks for speed
- Creating specialist agents with defined roles and handoff contracts
- Designing fault-tolerant AI workflows for production
---
## Pattern Selection Guide
```
Is the task sequential (each step needs previous output)?
YES → Sequential Pipeline
NO → Can tasks run in parallel?
YES → Parallel Fan-out/Fan-in
NO → Is there a hierarchy of decisions?
YES → Hierarchical Delegation
NO → Is it event-triggered?
YES → Event-Driven
NO → Need consensus/validation?
YES → Consensus Pattern
```
---
## Pattern 1: Sequential Pipeline
**Use when:** Each step depends on the previous output. Research → Draft → Review → Polish.
```python
# sequential_pipeline.py
from dataclasses import dataclass
from typing import Callable, Any
import anthropic
@dataclass
class PipelineStage:
name: "str"
system_prompt: str
input_key: str # what to take from state
output_key: str # what to write to state
model: str = "claude-3-5-sonnet-20241022"
max_tokens: int = 2048
class SequentialPipeline:
def __init__(self, stages: list[PipelineStage]):
self.stages = stages
self.client = anthropic.Anthropic()
def run(self, initial_input: str) -> dict:
state = {"input": initial_input}
for stage in self.stages:
print(f"[{stage.name}] Processing...")
stage_input = state.get(stage.input_key, "")
response = self.client.messages.create(
model=stage.model,
max_tokens=stage.max_tokens,
system=stage.system_prompt,
messages=[{"role": "user", "content": stage_input}],
)
state[stage.output_key] = response.content[0].text
state[f"{stage.name}_tokens"] = response.usage.input_tokens + response.usage.output_tokens
print(f"[{stage.name}] Done. Tokens: {state[f'{stage.name}_tokens']}")
return state
# Example: Blog post pipeline
pipeline = SequentialPipeline([
PipelineStage(
name="researcher",
system_prompt="You are a research specialist. Given a topic, produce a structured research brief with: key facts, statistics, expert perspectives, and controversy points.",
input_key="input",
output_key="research",
),
PipelineStage(
name="writer",
system_prompt="You are a senior content writer. Using the research provided, write a compelling 800-word blog post with a clear hook, 3 main sections, and a strong CTA.",
input_key="research",
output_key="draft",
),
PipelineStage(
name="editor",
system_prompt="You are a copy editor. Review the draft for: clarity, flow, grammar, and SEO. Return the improved version only, no commentary.",
input_key="draft",
output_key="final",
),
])
```
---
## Pattern 2: Parallel Fan-out / Fan-in
**Use when:** Independent tasks that can run concurrently. Research 5 competitors simultaneously.
```python
# parallel_fanout.py
import asyncio
import anthropic
from typing import Any
async def run_agent(client, task_name: "str-system-str-user-str-model-str"claude-3-5-sonnet-20241022") -> dict:
"""Single async agent call"""
loop = asyncio.get_event_loop()
def _call():
return client.messages.create(
model=model,
max_tokens=2048,
system=system,
messages=[{"role": "user", "content": user}],
)
response = await loop.run_in_executor(None, _call)
return {
"task": task_name,
"output": response.content[0].text,
"tokens": response.usage.input_tokens + response.usage.output_tokens,
}
async def parallel_research(competitors: list[str], research_type: str) -> dict:
"""Fan-out: research all competitors in parallel. Fan-in: synthesize results."""
client = anthropic.Anthropic()
# FAN-OUT: spawn parallel agent calls
tasks = [
run_agent(
client,
task_name=competitor,
system=f"You are a competitive intelligence analyst. Research {competitor} and provide: pricing, key features, target market, and known weaknesses.",
user=f"Analyze {competitor} for comparison with our product in the {research_type} market.",
)
for competitor in competitors
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Handle failures gracefully
successful = [r for r in results if not isinstance(r, Exception)]
failed = [r for r in results if isinstance(r, Exception)]
if failed:
print(f"Warning: {len(failed)} research tasks failed: {failed}")
# FAN-IN: synthesize
combined_research = "\n\n".join([
f"## {r['task']}\n{r['output']}" for r in successful
])
synthesis = await run_agent(
client,
task_name="synthesizer",
system="You are a strategic analyst. Synthesize competitor research into a concise comparison matrix and strategic recommendations.",
user=f"Synthesize these competitor analyses:\n\n{combined_research}",
model="claude-3-5-sonnet-20241022",
)
return {
"individual_analyses": successful,
"synthesis": synthesis["output"],
"total_tokens": sum(r["tokens"] for r in successful) + synthesis["tokens"],
}
```
---
## Pattern 3: Hierarchical Delegation
**Use when:** Complex tasks with subtask discovery. Orchestrator breaks down work, delegates to specialists.
```python
# hierarchical_delegation.py
import json
import anthropic
ORCHESTRATOR_SYSTEM = """You are an orchestration agent. Your job is to:
1. Analyze the user's request
2. Break it into subtasks
3. Assign each to the appropriate specialist agent
4. Collect results and synthesize
Available specialists:
- researcher: finds facts, data, and information
- writer: creates content and documents
- coder: writes and reviews code
- analyst: analyzes data and produces insights
Respond with a JSON plan:
{
"subtasks": [
{"id": "1", "agent": "researcher", "task": "...", "depends_on": []},
{"id": "2", "agent": "writer", "task": "...", "depends_on": ["1"]}
]
}"""
SPECIALIST_SYSTEMS = {
"researcher": "You are a research specialist. Find accurate, relevant information and cite sources when possible.",
"writer": "You are a professional writer. Create clear, engaging content in the requested format.",
"coder": "You are a senior software engineer. Write clean, well-commented code with error handling.",
"analyst": "You are a data analyst. Provide structured analysis with evidence-backed conclusions.",
}
class HierarchicalOrchestrator:
def __init__(self):
self.client = anthropic.Anthropic()
def run(self, user_request: str) -> str:
# 1. Orchestrator creates plan
plan_response = self.client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
system=ORCHESTRATOR_SYSTEM,
messages=[{"role": "user", "content": user_request}],
)
plan = json.loads(plan_response.content[0].text)
results = {}
# 2. Execute subtasks respecting dependencies
for subtask in self._topological_sort(plan["subtasks"]):
context = self._build_context(subtask, results)
specialist = SPECIALIST_SYSTEMS[subtask["agent"]]
result = self.client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
system=specialist,
messages=[{"role": "user", "content": f"{context}\n\nTask: {subtask['task']}"}],
)
results[subtask["id"]] = result.content[0].text
# 3. Final synthesis
all_results = "\n\n".join([f"### {k}\n{v}" for k, v in results.items()])
synthesis = self.client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
system="Synthesize the specialist outputs into a coherent final response.",
messages=[{"role": "user", "content": f"Original request: {user_request}\n\nSpecialist outputs:\n{all_results}"}],
)
return synthesis.content[0].text
def _build_context(self, subtask: dict, results: dict) -> str:
if not subtask.get("depends_on"):
return ""
deps = [f"Output from task {dep}:\n{results[dep]}" for dep in subtask["depends_on"] if dep in results]
return "Previous results:\n" + "\n\n".join(deps) if deps else ""
def _topological_sort(self, subtasks: list) -> list:
# Simple ordered execution respecting depends_on
ordered, remaining = [], list(subtasks)
completed = set()
while remaining:
for task in remaining:
if all(dep in completed for dep in task.get("depends_on", [])):
ordered.append(task)
completed.add(task["id"])
remaining.remove(task)
break
return ordered
```
---
## Handoff Protocol Template
```python
# Standard handoff context format — use between all agents
@dataclass
class AgentHandoff:
"""Structured context passed between agents in a workflow."""
task_id: str
workflow_id: str
step_number: int
total_steps: int
# What was done
previous_agent: str
previous_output: str
artifacts: dict # {"filename": "content"} for any files produced
# What to do next
current_agent: str
current_task: str
constraints: list[str] # hard rules for this step
# Metadata
context_budget_remaining: int # tokens left for this agent
cost_so_far_usd: float
def to_prompt(self) -> str:
return f"""
# Agent Handoff — Step {self.step_number}/{self.total_steps}
## Your Task
{self.current_task}
## Constraints
{chr(10).join(f'- {c}' for c in self.constraints)}
## Context from Previous Step ({self.previous_agent})
{self.previous_output[:2000]}{"... [truncated]" if len(self.previous_output) > 2000 else ""}
## Context Budget
You have approximately {self.context_budget_remaining} tokens remaining. Be concise.
"""
```
---
## Error Recovery Patterns
```python
import time
from functools import wraps
def with_retry(max_attempts=3, backoff_seconds=2, fallback_model=None):
"""Decorator for agent calls with exponential backoff and model fallback."""
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(max_attempts):
try:
return fn(*args, **kwargs)
except Exception as e:
last_error = e
if attempt < max_attempts - 1:
wait = backoff_seconds * (2 ** attempt)
print(f"Attempt {attempt+1} failed: {e}. Retrying in {wait}s...")
time.sleep(wait)
# Fall back to cheaper/faster model on rate limit
if fallback_model and "rate_limit" in str(e).lower():
kwargs["model"] = fallback_model
raise last_error
return wrapper
return decorator
@with_retry(max_attempts=3, fallback_model="claude-3-haiku-20240307")
def call_agent(model, system, user):
...
```
---
## Context Window Budgeting
```python
# Budget context across a multi-step pipeline
# Rule: never let any step consume more than 60% of remaining budget
CONTEXT_LIMITS = {
"claude-3-5-sonnet-20241022": 200_000,
"gpt-4o": 128_000,
}
class ContextBudget:
def __init__(self, model: str, reserve_pct: float = 0.2):
total = CONTEXT_LIMITS.get(model, 128_000)
self.total = total
self.reserve = int(total * reserve_pct) # keep 20% as buffer
self.used = 0
@property
def remaining(self):
return self.total - self.reserve - self.used
def allocate(self, step_name: "str-requested-int-int"
allocated = min(requested, int(self.remaining * 0.6)) # max 60% of remaining
print(f"[Budget] {step_name}: allocated {allocated:,} tokens (remaining: {self.remaining:,})")
return allocated
def consume(self, tokens_used: int):
self.used += tokens_used
def truncate_to_budget(text: str, token_budget: int, chars_per_token: float = 4.0) -> str:
"""Rough truncation — use tiktoken for precision."""
char_budget = int(token_budget * chars_per_token)
if len(text) <= char_budget:
return text
return text[:char_budget] + "\n\n[... truncated to fit context budget ...]"
```
---
## Cost Optimization Strategies
| Strategy | Savings | Tradeoff |
|---|---|---|
| Use Haiku for routing/classification | 85-90% | Slightly less nuanced judgment |
| Cache repeated system prompts | 50-90% | Requires prompt caching setup |
| Truncate intermediate outputs | 20-40% | May lose detail in handoffs |
| Batch similar tasks | 50% | Latency increases |
| Use Sonnet for most, Opus for final step only | 60-70% | Final quality may improve |
| Short-circuit on confidence threshold | 30-50% | Need confidence scoring |
---
## Common Pitfalls
- **Circular dependencies** — agents calling each other in loops; enforce DAG structure at design time
- **Context bleed** — passing entire previous output to every step; summarize or extract only what's needed
- **No timeout** — a stuck agent blocks the whole pipeline; always set max_tokens and wall-clock timeouts
- **Silent failures** — agent returns plausible but wrong output; add validation steps for critical paths
- **Ignoring cost** — 10 parallel Opus calls is $0.50 per workflow; model selection is a cost decision
- **Over-orchestration** — if a single prompt can do it, it should; only add agents when genuinely needed
Stripe Integration Expert
---
name: "stripe-integration-expert"
description: "Stripe Integration Expert"
---
# Stripe Integration Expert
**Tier:** POWERFUL
**Category:** Engineering Team
**Domain:** Payments / Billing Infrastructure
---
## Overview
Implement production-grade Stripe integrations: subscriptions with trials and proration, one-time payments, usage-based billing, checkout sessions, idempotent webhook handlers, customer portal, and invoicing. Covers Next.js, Express, and Django patterns.
---
## Core Capabilities
- Subscription lifecycle management (create, upgrade, downgrade, cancel, pause)
- Trial handling and conversion tracking
- Proration calculation and credit application
- Usage-based billing with metered pricing
- Idempotent webhook handlers with signature verification
- Customer portal integration
- Invoice generation and PDF access
- Full Stripe CLI local testing setup
---
## When to Use
- Adding subscription billing to any web app
- Implementing plan upgrades/downgrades with proration
- Building usage-based or seat-based billing
- Debugging webhook delivery failures
- Migrating from one billing model to another
---
## Subscription Lifecycle State Machine
```
FREE_TRIAL ──paid──► ACTIVE ──cancel──► CANCEL_PENDING ──period_end──► CANCELED
│ │ │
│ downgrade reactivate
│ ▼ │
│ DOWNGRADING ──period_end──► ACTIVE (lower plan) │
│ │
└──trial_end without payment──► PAST_DUE ──payment_failed 3x──► CANCELED
│
payment_success
│
▼
ACTIVE
```
### DB subscription status values:
`trialing | active | past_due | canceled | cancel_pending | paused | unpaid`
---
## Stripe Client Setup
```typescript
// lib/stripe.ts
import Stripe from "stripe"
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-04-10",
typescript: true,
appInfo: {
name: "myapp",
version: "1.0.0",
},
})
// Price IDs by plan (set in env)
export const PLANS = {
starter: {
monthly: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID!,
yearly: process.env.STRIPE_STARTER_YEARLY_PRICE_ID!,
features: ["5 projects", "10k events"],
},
pro: {
monthly: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!,
yearly: process.env.STRIPE_PRO_YEARLY_PRICE_ID!,
features: ["Unlimited projects", "1M events"],
},
} as const
```
---
## Checkout Session (Next.js App Router)
```typescript
// app/api/billing/checkout/route.ts
import { NextResponse } from "next/server"
import { stripe } from "@/lib/stripe"
import { getAuthUser } from "@/lib/auth"
import { db } from "@/lib/db"
export async function POST(req: Request) {
const user = await getAuthUser()
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const { priceId, interval = "monthly" } = await req.json()
// Get or create Stripe customer
let stripeCustomerId = user.stripeCustomerId
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
email: user.email,
name: "username-undefined"
metadata: { userId: user.id },
})
stripeCustomerId = customer.id
await db.user.update({ where: { id: user.id }, data: { stripeCustomerId } })
}
const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
allow_promotion_codes: true,
subscription_data: {
trial_period_days: user.hasHadTrial ? undefined : 14,
metadata: { userId: user.id },
},
success_url: `process.env.NEXT_PUBLIC_APP_URL/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `process.env.NEXT_PUBLIC_APP_URL/pricing`,
metadata: { userId: user.id },
})
return NextResponse.json({ url: session.url })
}
```
---
## Subscription Upgrade/Downgrade
```typescript
// lib/billing.ts
export async function changeSubscriptionPlan(
subscriptionId: string,
newPriceId: string,
immediate = false
) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId)
const currentItem = subscription.items.data[0]
if (immediate) {
// Upgrade: apply immediately with proration
return stripe.subscriptions.update(subscriptionId, {
items: [{ id: currentItem.id, price: newPriceId }],
proration_behavior: "always_invoice",
billing_cycle_anchor: "unchanged",
})
} else {
// Downgrade: apply at period end, no proration
return stripe.subscriptions.update(subscriptionId, {
items: [{ id: currentItem.id, price: newPriceId }],
proration_behavior: "none",
billing_cycle_anchor: "unchanged",
})
}
}
// Preview proration before confirming upgrade
export async function previewProration(subscriptionId: string, newPriceId: string) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId)
const prorationDate = Math.floor(Date.now() / 1000)
const invoice = await stripe.invoices.retrieveUpcoming({
customer: subscription.customer as string,
subscription: subscriptionId,
subscription_items: [{ id: subscription.items.data[0].id, price: newPriceId }],
subscription_proration_date: prorationDate,
})
return {
amountDue: invoice.amount_due,
prorationDate,
lineItems: invoice.lines.data,
}
}
```
---
## Complete Webhook Handler (Idempotent)
```typescript
// app/api/webhooks/stripe/route.ts
import { NextResponse } from "next/server"
import { headers } from "next/headers"
import { stripe } from "@/lib/stripe"
import { db } from "@/lib/db"
import Stripe from "stripe"
// Processed events table to ensure idempotency
async function hasProcessedEvent(eventId: string): Promise<boolean> {
const existing = await db.stripeEvent.findUnique({ where: { id: eventId } })
return !!existing
}
async function markEventProcessed(eventId: string, type: string) {
await db.stripeEvent.create({ data: { id: eventId, type, processedAt: new Date() } })
}
export async function POST(req: Request) {
const body = await req.text()
const signature = headers().get("stripe-signature")!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!)
} catch (err) {
console.error("Webhook signature verification failed:", err)
return NextResponse.json({ error: "Invalid signature" }, { status: 400 })
}
// Idempotency check
if (await hasProcessedEvent(event.id)) {
return NextResponse.json({ received: true, skipped: true })
}
try {
switch (event.type) {
case "checkout.session.completed":
await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session)
break
case "customer.subscription.created":
case "customer.subscription.updated":
await handleSubscriptionUpdated(event.data.object as Stripe.Subscription)
break
case "customer.subscription.deleted":
await handleSubscriptionDeleted(event.data.object as Stripe.Subscription)
break
case "invoice.payment_succeeded":
await handleInvoicePaymentSucceeded(event.data.object as Stripe.Invoice)
break
case "invoice.payment_failed":
await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice)
break
default:
console.log(`Unhandled event type: event.type`)
}
await markEventProcessed(event.id, event.type)
return NextResponse.json({ received: true })
} catch (err) {
console.error(`Error processing webhook event.type:`, err)
// Return 500 so Stripe retries — don't mark as processed
return NextResponse.json({ error: "Processing failed" }, { status: 500 })
}
}
async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
if (session.mode !== "subscription") return
const userId = session.metadata?.userId
if (!userId) throw new Error("No userId in checkout session metadata")
const subscription = await stripe.subscriptions.retrieve(session.subscription as string)
await db.user.update({
where: { id: userId },
data: {
stripeCustomerId: session.customer as string,
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
subscriptionStatus: subscription.status,
hasHadTrial: true,
},
})
}
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
const user = await db.user.findUnique({
where: { stripeSubscriptionId: subscription.id },
})
if (!user) {
// Look up by customer ID as fallback
const customer = await db.user.findUnique({
where: { stripeCustomerId: subscription.customer as string },
})
if (!customer) throw new Error(`No user found for subscription subscription.id`)
}
await db.user.update({
where: { stripeSubscriptionId: subscription.id },
data: {
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
subscriptionStatus: subscription.status,
cancelAtPeriodEnd: subscription.cancel_at_period_end,
},
})
}
async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
await db.user.update({
where: { stripeSubscriptionId: subscription.id },
data: {
stripeSubscriptionId: null,
stripePriceId: null,
stripeCurrentPeriodEnd: null,
subscriptionStatus: "canceled",
},
})
}
async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) {
if (!invoice.subscription) return
const attemptCount = invoice.attempt_count
await db.user.update({
where: { stripeSubscriptionId: invoice.subscription as string },
data: { subscriptionStatus: "past_due" },
})
if (attemptCount >= 3) {
// Send final dunning email
await sendDunningEmail(invoice.customer_email!, "final")
} else {
await sendDunningEmail(invoice.customer_email!, "retry")
}
}
async function handleInvoicePaymentSucceeded(invoice: Stripe.Invoice) {
if (!invoice.subscription) return
await db.user.update({
where: { stripeSubscriptionId: invoice.subscription as string },
data: {
subscriptionStatus: "active",
stripeCurrentPeriodEnd: new Date(invoice.period_end * 1000),
},
})
}
```
---
## Usage-Based Billing
```typescript
// Report usage for metered subscriptions
export async function reportUsage(subscriptionItemId: string, quantity: number) {
await stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
quantity,
timestamp: Math.floor(Date.now() / 1000),
action: "increment",
})
}
// Example: report API calls in middleware
export async function trackApiCall(userId: string) {
const user = await db.user.findUnique({ where: { id: userId } })
if (user?.stripeSubscriptionId) {
const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId)
const meteredItem = subscription.items.data.find(
(item) => item.price.recurring?.usage_type === "metered"
)
if (meteredItem) {
await reportUsage(meteredItem.id, 1)
}
}
}
```
---
## Customer Portal
```typescript
// app/api/billing/portal/route.ts
import { NextResponse } from "next/server"
import { stripe } from "@/lib/stripe"
import { getAuthUser } from "@/lib/auth"
export async function POST() {
const user = await getAuthUser()
if (!user?.stripeCustomerId) {
return NextResponse.json({ error: "No billing account" }, { status: 400 })
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: `process.env.NEXT_PUBLIC_APP_URL/settings/billing`,
})
return NextResponse.json({ url: portalSession.url })
}
```
---
## Testing with Stripe CLI
```bash
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to local dev
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger specific events for testing
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed
# Test with specific customer
stripe trigger customer.subscription.updated \
--override subscription:customer=cus_xxx
# View recent events
stripe events list --limit 10
# Test cards
# Success: 4242 4242 4242 4242
# Requires auth: 4000 0025 0000 3155
# Decline: 4000 0000 0000 9995
# Insufficient funds: 4000 0000 0000 9995
```
---
## Feature Gating Helper
```typescript
// lib/subscription.ts
export function isSubscriptionActive(user: { subscriptionStatus: string | null, stripeCurrentPeriodEnd: Date | null }) {
if (!user.subscriptionStatus) return false
if (user.subscriptionStatus === "active" || user.subscriptionStatus === "trialing") return true
// Grace period: past_due but not yet expired
if (user.subscriptionStatus === "past_due" && user.stripeCurrentPeriodEnd) {
return user.stripeCurrentPeriodEnd > new Date()
}
return false
}
// Middleware usage
export async function requireActiveSubscription() {
const user = await getAuthUser()
if (!isSubscriptionActive(user)) {
redirect("/billing?reason=subscription_required")
}
}
```
---
## Common Pitfalls
- **Webhook delivery order not guaranteed** — always re-fetch from Stripe API, never trust event data alone for DB updates
- **Double-processing webhooks** — Stripe retries on 500; always use idempotency table
- **Trial conversion tracking** — store `hasHadTrial: true` in DB to prevent trial abuse
- **Proration surprises** — always preview proration before upgrade; show user the amount before confirming
- **Customer portal not configured** — must enable features in Stripe dashboard under Billing → Customer portal settings
- **Missing metadata on checkout** — always pass `userId` in metadata; can't link subscription to user without it
Email Template Builder
---
name: "email-template-builder"
description: "Email Template Builder"
---
# Email Template Builder
**Tier:** POWERFUL
**Category:** Engineering Team
**Domain:** Transactional Email / Communications Infrastructure
---
## Overview
Build complete transactional email systems: React Email templates, provider integration, preview server, i18n support, dark mode, spam optimization, and analytics tracking. Output production-ready code for Resend, Postmark, SendGrid, or AWS SES.
---
## Core Capabilities
- React Email templates (welcome, verification, password reset, invoice, notification, digest)
- MJML templates for maximum email client compatibility
- Multi-provider support with unified sending interface
- Local preview server with hot reload
- i18n/localization with typed translation keys
- Dark mode support using media queries
- Spam score optimization checklist
- Open/click tracking with UTM parameters
---
## When to Use
- Setting up transactional email for a new product
- Migrating from a legacy email system
- Adding new email types (invoice, digest, notification)
- Debugging email deliverability issues
- Implementing i18n for email templates
---
## Project Structure
```
emails/
├── components/
│ ├── layout/
│ │ ├── email-layout.tsx # Base layout with brand header/footer
│ │ └── email-button.tsx # CTA button component
│ ├── partials/
│ │ ├── header.tsx
│ │ └── footer.tsx
├── templates/
│ ├── welcome.tsx
│ ├── verify-email.tsx
│ ├── password-reset.tsx
│ ├── invoice.tsx
│ ├── notification.tsx
│ └── weekly-digest.tsx
├── lib/
│ ├── send.ts # Unified send function
│ ├── providers/
│ │ ├── resend.ts
│ │ ├── postmark.ts
│ │ └── ses.ts
│ └── tracking.ts # UTM + analytics
├── i18n/
│ ├── en.ts
│ └── de.ts
└── preview/ # Dev preview server
└── server.ts
```
---
## Base Email Layout
```tsx
// emails/components/layout/email-layout.tsx
import {
Body, Container, Head, Html, Img, Preview, Section, Text, Hr, Font
} from "@react-email/components"
interface EmailLayoutProps {
preview: string
children: React.ReactNode
}
export function EmailLayout({ preview, children }: EmailLayoutProps) {
return (
<Html lang="en">
<Head>
<Font
fontFamily="Inter"
fallbackFontFamily="Arial"
webFont={{ url: "https://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiJ-Ek-_EeA.woff2", format: "woff2" }}
fontWeight={400}
fontStyle="normal"
/>
{/* Dark mode styles */}
<style>{`
@media (prefers-color-scheme: dark) {
.email-body { background-color: #0f0f0f !important; }
.email-container { background-color: #1a1a1a !important; }
.email-text { color: #e5e5e5 !important; }
.email-heading { color: #ffffff !important; }
.email-divider { border-color: #333333 !important; }
}
`}</style>
</Head>
<Preview>{preview}</Preview>
<Body className="email-body" style={styles.body}>
<Container className="email-container" style={styles.container}>
{/* Header */}
<Section style={styles.header}>
<Img src="https://yourapp.com/logo.png" width={120} height={40} alt="MyApp" />
</Section>
{/* Content */}
<Section style={styles.content}>
{children}
</Section>
{/* Footer */}
<Hr style={styles.divider} />
<Section style={styles.footer}>
<Text style={styles.footerText}>
MyApp Inc. · 123 Main St · San Francisco, CA 94105
</Text>
<Text style={styles.footerText}>
<a href="{{unsubscribe_url}}" style={styles.link}>Unsubscribe</a>
{" · "}
<a href="https://yourapp.com/privacy" style={styles.link}>Privacy Policy</a>
</Text>
</Section>
</Container>
</Body>
</Html>
)
}
const styles = {
body: { backgroundColor: "#f5f5f5", fontFamily: "Inter, Arial, sans-serif" },
container: { maxWidth: "600px", margin: "0 auto", backgroundColor: "#ffffff", borderRadius: "8px", overflow: "hidden" },
header: { padding: "24px 32px", borderBottom: "1px solid #e5e5e5" },
content: { padding: "32px" },
divider: { borderColor: "#e5e5e5", margin: "0 32px" },
footer: { padding: "24px 32px" },
footerText: { fontSize: "12px", color: "#6b7280", textAlign: "center" as const, margin: "4px 0" },
link: { color: "#6b7280", textDecoration: "underline" },
}
```
---
## Welcome Email
```tsx
// emails/templates/welcome.tsx
import { Button, Heading, Text } from "@react-email/components"
import { EmailLayout } from "../components/layout/email-layout"
interface WelcomeEmailProps {
name: "string"
confirmUrl: string
trialDays?: number
}
export function WelcomeEmail({ name, confirmUrl, trialDays = 14 }: WelcomeEmailProps) {
return (
<EmailLayout preview={`Welcome to MyApp, name! Confirm your email to get started.`}>
<Heading style={styles.h1}>Welcome to MyApp, {name}!</Heading>
<Text style={styles.text}>
We're excited to have you on board. You've got {trialDays} days to explore everything MyApp has to offer — no credit card required.
</Text>
<Text style={styles.text}>
First, confirm your email address to activate your account:
</Text>
<Button href={confirmUrl} style={styles.button}>
Confirm Email Address
</Button>
<Text style={styles.hint}>
Button not working? Copy and paste this link into your browser:
<br />
<a href={confirmUrl} style={styles.link}>{confirmUrl}</a>
</Text>
<Text style={styles.text}>
Once confirmed, you can:
</Text>
<ul style={styles.list}>
<li>Connect your first project in 2 minutes</li>
<li>Invite your team (free for up to 3 members)</li>
<li>Set up Slack notifications</li>
</ul>
</EmailLayout>
)
}
export default WelcomeEmail
const styles = {
h1: { fontSize: "28px", fontWeight: "700", color: "#111827", margin: "0 0 16px" },
text: { fontSize: "16px", lineHeight: "1.6", color: "#374151", margin: "0 0 16px" },
button: { backgroundColor: "#4f46e5", color: "#ffffff", borderRadius: "6px", fontSize: "16px", fontWeight: "600", padding: "12px 24px", textDecoration: "none", display: "inline-block", margin: "8px 0 24px" },
hint: { fontSize: "13px", color: "#6b7280" },
link: { color: "#4f46e5" },
list: { fontSize: "16px", lineHeight: "1.8", color: "#374151", paddingLeft: "20px" },
}
```
---
## Invoice Email
```tsx
// emails/templates/invoice.tsx
import { Row, Column, Section, Heading, Text, Hr, Button } from "@react-email/components"
import { EmailLayout } from "../components/layout/email-layout"
interface InvoiceItem { description: string; amount: number }
interface InvoiceEmailProps {
name: "string"
invoiceNumber: string
invoiceDate: string
dueDate: string
items: InvoiceItem[]
total: number
currency: string
downloadUrl: string
}
export function InvoiceEmail({ name, invoiceNumber, invoiceDate, dueDate, items, total, currency = "USD", downloadUrl }: InvoiceEmailProps) {
const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency })
return (
<EmailLayout preview={`Invoice invoiceNumber - formatter.format(total / 100)`}>
<Heading style={styles.h1}>Invoice #{invoiceNumber}</Heading>
<Text style={styles.text}>Hi {name},</Text>
<Text style={styles.text}>Here's your invoice from MyApp. Thank you for your continued support.</Text>
{/* Invoice Meta */}
<Section style={styles.metaBox}>
<Row>
<Column><Text style={styles.metaLabel}>Invoice Date</Text><Text style={styles.metaValue}>{invoiceDate}</Text></Column>
<Column><Text style={styles.metaLabel}>Due Date</Text><Text style={styles.metaValue}>{dueDate}</Text></Column>
<Column><Text style={styles.metaLabel}>Amount Due</Text><Text style={styles.metaValueLarge}>{formatter.format(total / 100)}</Text></Column>
</Row>
</Section>
{/* Line Items */}
<Section style={styles.table}>
<Row style={styles.tableHeader}>
<Column><Text style={styles.tableHeaderText}>Description</Text></Column>
<Column><Text style={{ ...styles.tableHeaderText, textAlign: "right" }}>Amount</Text></Column>
</Row>
{items.map((item, i) => (
<Row key={i} style={i % 2 === 0 ? styles.tableRowEven : styles.tableRowOdd}>
<Column><Text style={styles.tableCell}>{item.description}</Text></Column>
<Column><Text style={{ ...styles.tableCell, textAlign: "right" }}>{formatter.format(item.amount / 100)}</Text></Column>
</Row>
))}
<Hr style={styles.divider} />
<Row>
<Column><Text style={styles.totalLabel}>Total</Text></Column>
<Column><Text style={styles.totalValue}>{formatter.format(total / 100)}</Text></Column>
</Row>
</Section>
<Button href={downloadUrl} style={styles.button}>Download PDF Invoice</Button>
</EmailLayout>
)
}
export default InvoiceEmail
const styles = {
h1: { fontSize: "24px", fontWeight: "700", color: "#111827", margin: "0 0 16px" },
text: { fontSize: "15px", lineHeight: "1.6", color: "#374151", margin: "0 0 12px" },
metaBox: { backgroundColor: "#f9fafb", borderRadius: "8px", padding: "16px", margin: "16px 0" },
metaLabel: { fontSize: "12px", color: "#6b7280", fontWeight: "600", textTransform: "uppercase" as const, margin: "0 0 4px" },
metaValue: { fontSize: "14px", color: "#111827", margin: 0 },
metaValueLarge: { fontSize: "20px", fontWeight: "700", color: "#4f46e5", margin: 0 },
table: { width: "100%", margin: "16px 0" },
tableHeader: { backgroundColor: "#f3f4f6", borderRadius: "4px" },
tableHeaderText: { fontSize: "12px", fontWeight: "600", color: "#374151", padding: "8px 12px", textTransform: "uppercase" as const },
tableRowEven: { backgroundColor: "#ffffff" },
tableRowOdd: { backgroundColor: "#f9fafb" },
tableCell: { fontSize: "14px", color: "#374151", padding: "10px 12px" },
divider: { borderColor: "#e5e5e5", margin: "8px 0" },
totalLabel: { fontSize: "16px", fontWeight: "700", color: "#111827", padding: "8px 12px" },
totalValue: { fontSize: "16px", fontWeight: "700", color: "#111827", textAlign: "right" as const, padding: "8px 12px" },
button: { backgroundColor: "#4f46e5", color: "#fff", borderRadius: "6px", padding: "12px 24px", fontSize: "15px", fontWeight: "600", textDecoration: "none" },
}
```
---
## Unified Send Function
```typescript
// emails/lib/send.ts
import { Resend } from "resend"
import { render } from "@react-email/render"
import { WelcomeEmail } from "../templates/welcome"
import { InvoiceEmail } from "../templates/invoice"
import { addTrackingParams } from "./tracking"
const resend = new Resend(process.env.RESEND_API_KEY)
type EmailPayload =
| { type: "welcome"; props: Parameters<typeof WelcomeEmail>[0] }
| { type: "invoice"; props: Parameters<typeof InvoiceEmail>[0] }
export async function sendEmail(to: string, payload: EmailPayload) {
const templates = {
welcome: { component: WelcomeEmail, subject: "Welcome to MyApp — confirm your email" },
invoice: { component: InvoiceEmail, subject: `Invoice from MyApp` },
}
const template = templates[payload.type]
const html = render(template.component(payload.props as any))
const trackedHtml = addTrackingParams(html, { campaign: payload.type })
const result = await resend.emails.send({
from: "MyApp <[email protected]>",
to,
subject: template.subject,
html: trackedHtml,
tags: [{ name: "email-type", value: payload.type }],
})
return result
}
```
---
## Preview Server Setup
```typescript
// package.json scripts
{
"scripts": {
"email:dev": "email dev --dir emails/templates --port 3001",
"email:build": "email export --dir emails/templates --outDir emails/out"
}
}
// Run: npm run email:dev
// Opens: http://localhost:3001
// Shows all templates with live preview and hot reload
```
---
## i18n Support
```typescript
// emails/i18n/en.ts
export const en = {
welcome: {
preview: (name: "string-welcome-to-myapp-name"
heading: (name: "string-welcome-to-myapp-name"
body: (days: number) => `You've got days days to explore everything.`,
cta: "Confirm Email Address",
},
}
// emails/i18n/de.ts
export const de = {
welcome: {
preview: (name: "string-willkommen-bei-myapp-name"
heading: (name: "string-willkommen-bei-myapp-name"
body: (days: number) => `Du hast days Tage Zeit, alles zu erkunden.`,
cta: "E-Mail-Adresse bestätigen",
},
}
// Usage in template
import { en, de } from "../i18n"
const t = locale === "de" ? de : en
```
---
## Spam Score Optimization Checklist
- [ ] Sender domain has SPF, DKIM, and DMARC records configured
- [ ] From address uses your own domain (not gmail.com/hotmail.com)
- [ ] Subject line under 50 characters, no ALL CAPS, no "FREE!!!"
- [ ] Text-to-image ratio: at least 60% text
- [ ] Plain text version included alongside HTML
- [ ] Unsubscribe link in every marketing email (CAN-SPAM, GDPR)
- [ ] No URL shorteners — use full branded links
- [ ] No red-flag words: "guarantee", "no risk", "limited time offer" in subject
- [ ] Single CTA per email — no 5 different buttons
- [ ] Image alt text on every image
- [ ] HTML validates — no broken tags
- [ ] Test with Mail-Tester.com before first send (target: 9+/10)
---
## Analytics Tracking
```typescript
// emails/lib/tracking.ts
interface TrackingParams {
campaign: string
medium?: string
source?: string
}
export function addTrackingParams(html: string, params: TrackingParams): string {
const utmString = new URLSearchParams({
utm_source: params.source ?? "email",
utm_medium: params.medium ?? "transactional",
utm_campaign: params.campaign,
}).toString()
// Add UTM params to all links in the email
return html.replace(/href="(https?:\/\/[^"]+)"/g, (match, url) => {
const separator = url.includes("?") ? "&" : "?"
return `href="urlseparatorutmString"`
})
}
```
---
## Common Pitfalls
- **Inline styles required** — most email clients strip `<head>` styles; React Email handles this
- **Max width 600px** — anything wider breaks on Gmail mobile
- **No flexbox/grid** — use `<Row>` and `<Column>` from react-email, not CSS grid
- **Dark mode media queries** — must use `!important` to override inline styles
- **Missing plain text** — all major providers have a plain text field; always populate it
- **Transactional vs marketing** — use separate sending domains/IPs to protect deliverability
When the user wants to audit, redesign, or plan their website's structure, URL hierarchy, navigation design, or internal linking strategy. Use when the user...
---
name: "site-architecture"
description: "When the user wants to audit, redesign, or plan their website's structure, URL hierarchy, navigation design, or internal linking strategy. Use when the user mentions 'site architecture,' 'URL structure,' 'internal links,' 'site navigation,' 'breadcrumbs,' 'topic clusters,' 'hub pages,' 'orphan pages,' 'silo structure,' 'information architecture,' or 'website reorganization.' Also use when someone has SEO problems and the root cause is structural (not content or schema). NOT for content strategy decisions about what to write (use content-strategy) or for schema markup (use schema-markup)."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Site Architecture & Internal Linking
You are an expert in website information architecture and technical SEO structure. Your goal is to design website architecture that makes it easy for users to navigate, easy for search engines to crawl, and builds topical authority through intelligent internal linking.
## Before Starting
**Check for context first:**
If `marketing-context.md` exists, read it before asking questions.
Gather this context:
### 1. Current State
- Do they have an existing site? (URL, CMS, sitemap.xml available?)
- How many pages exist? Rough estimate by section.
- What are the top-performing pages (if they know)?
- Any known problems: orphan pages, duplicate content, poor rankings?
### 2. Goals
- Primary business goal (lead gen, e-commerce, content authority, local search)
- Target audience and their mental model of navigation
- Specific SEO targets — topics or keyword clusters they want to rank for
### 3. Constraints
- CMS capabilities (can they change URLs? Does it auto-generate certain structures?)
- Redirect capacity (if restructuring, can they manage bulk 301s?)
- Development resources (minor tweaks vs full migration)
---
## How This Skill Works
### Mode 1: Audit Current Architecture
When a site exists and they need a structural assessment.
1. Run `scripts/sitemap_analyzer.py` on their sitemap.xml (or paste sitemap content)
2. Review: depth distribution, URL patterns, potential orphans, duplicate paths
3. Evaluate navigation by reviewing the site manually or from their description
4. Identify the top structural problems by SEO impact
5. Deliver a prioritized audit with quick wins and structural recommendations
### Mode 2: Plan New Structure
When building a new site or doing a full redesign/restructure.
1. Map business goals to site sections
2. Design URL hierarchy (flat vs layered by content type)
3. Define content silos for topical authority
4. Plan navigation zones: primary nav, breadcrumbs, footer nav, contextual
5. Deliver site map diagram (text-based tree) + URL structure spec
### Mode 3: Internal Linking Strategy
When the structure is fine but they need to improve link equity flow and topical signals.
1. Identify hub pages (the pillar content that should rank highest)
2. Map spoke pages (supporting content that links to hubs)
3. Find orphan pages (indexed pages with no inbound internal links)
4. Identify anchor text patterns and over-optimized phrases
5. Deliver an internal linking plan: which pages link to which, with anchor text guidance
---
## URL Structure Principles
### The Core Rule: URLs are for Humans First
A URL should tell a user exactly where they are before they click. It also tells search engines about content hierarchy. Get this right once — URL changes later require redirects and lose equity.
### Flat vs Layered: Pick the Right Depth
| Depth | Example | Use When |
|-------|---------|----------|
| Flat (1 level) | `/blog/cold-email-tips` | Blog posts, articles, standalone pages |
| Two levels | `/blog/email-marketing/cold-email-tips` | When category is a ranking page itself |
| Three levels | `/solutions/marketing/email-automation` | Product families, nested services |
| 4+ levels | `/a/b/c/d/page` | ❌ Avoid — dilutes crawl equity, confusing |
**Rule of thumb:** If the category URL (`/blog/email-marketing/`) is not a real page you want to rank, don't create the directory. Flat is usually better for SEO.
### URL Construction Rules
| Do | Don't |
|----|-------|
| `/how-to-write-cold-emails` | `/how_to_write_cold_emails` (underscores) |
| `/pricing` | `/pricing-page` (redundant suffixes) |
| `/blog/seo-tips-2024` | `/blog/article?id=4827` (dynamic, non-descriptive) |
| `/services/web-design` | `/services/web-design/` (trailing slash — pick one and be consistent) |
| `/about` | `/about-us-company-info` (keyword stuffing the URL) |
| Short, human-readable | Long, generated, token-filled |
### Keywords in URLs
Yes — include the primary keyword. No — don't stuff 4 keywords in.
`/guides/technical-seo-audit` ✅
`/guides/technical-seo-audit-checklist-how-to-complete-step-by-step` ❌
The keyword in the URL is a minor signal, not a major one. Don't sacrifice readability for it.
### Reference docs
See `references/url-design-guide.md` for patterns by site type (blog, SaaS, e-commerce, local).
---
## Navigation Design
Navigation serves two masters: user experience and link equity flow. Most sites optimize for neither.
### Navigation Zones
| Zone | Purpose | SEO Role |
|------|---------|----------|
| Primary nav | Core site sections, 5-8 items max | Passes equity to top-level pages |
| Secondary nav | Sub-sections within a section | Passes equity within a silo |
| Breadcrumbs | Current location in hierarchy | Equity from deep pages upward |
| Footer nav | Secondary utility links, key service pages | Sitewide links — use carefully |
| Contextual nav | In-content links, related posts, "next step" links | Most powerful equity signal |
| Sidebar | Related content, category listing | Medium equity if above fold |
### Primary Navigation Rules
- 5-8 items maximum. Cognitive load increases with every item.
- Each nav item should link to a page you want to rank.
- Never use nav labels like "Resources" with no landing page — it should be a real, rankable resources page.
- Dropdown menus are fine but crawlers may not engage them deeply — critical pages need a clickable parent link.
### Breadcrumbs
Add breadcrumbs to every non-homepage page. They do three things:
1. Show users where they are
2. Create site-wide upward internal links to category/hub pages
3. Enable BreadcrumbList schema for rich results in Google
Format: `Home > Category > Subcategory > Current Page`
Every breadcrumb segment should be a real, crawlable link — not just styled text.
---
## Silo Structure & Topical Authority
A silo is a self-contained cluster of content about one topic, where all pages link to each other and to a central hub page. Google uses this to determine topical authority.
### Hub-and-Spoke Model
```
HUB: /seo/ ← Pillar page, broad topic
SPOKE: /seo/technical-seo/ ← Sub-topic
SPOKE: /seo/on-page-seo/ ← Sub-topic
SPOKE: /seo/link-building/ ← Sub-topic
SPOKE: /seo/keyword-research/ ← Sub-topic
└─ DEEP: /seo/keyword-research/long-tail-keywords/ ← Specific guide
```
**Linking rules within a silo:**
- Hub links to all spokes
- Each spoke links back to hub
- Spokes can link to adjacent spokes (contextually relevant)
- Deep pages link up to their spoke + the hub
- Cross-silo links are fine when genuinely relevant — just don't build a link for its own sake
### Building Topic Clusters
1. Identify your core topics (usually 3-7 for a focused site)
2. For each topic: one pillar page (the hub) that covers it broadly
3. Create spoke content for each major sub-question within the topic
4. Every spoke links to the pillar with relevant anchor text
5. The pillar links down to all spokes
6. Build the cluster before you build the links — if you don't have the content, the links don't help
---
## Internal Linking Strategy
Internal links are the most underused SEO lever. They're fully under your control, free, and directly affect which pages rank.
### Link Equity Principles
- Google crawls your site from the homepage outward
- Pages closer to the homepage (fewer clicks away) get more equity
- A page with no internal links is an orphan — Google won't prioritize it
- Anchor text matters: generic ("click here") signals nothing; descriptive ("cold email templates") signals topic relevance
### Anchor Text Rules
| Type | Example | Use |
|------|---------|-----|
| Exact match | "cold email templates" | Use sparingly — 1-2x per page, looks natural |
| Partial match | "writing effective cold emails" | Primary approach — most internal links |
| Branded | "our email guide" | Fine, not the most powerful |
| Generic | "click here", "learn more" | Avoid — wastes the signal |
| Naked URL | `https://example.com/guide` | Never use for internal links |
### Finding and Fixing Orphan Pages
An orphan page is indexed but has no inbound internal links. It's invisible to the site's link graph.
How to find them:
1. Export all indexed URLs (from GSC, Screaming Frog, or `sitemap_analyzer.py`)
2. Export all internal links on the site
3. Pages that appear in set A but not set B are orphans
4. Or: run `scripts/sitemap_analyzer.py` which flags potential orphan candidates
How to fix them:
- Add contextual links from relevant existing pages
- Add them to relevant hub pages
- If they truly have no home, consider whether they should exist at all
### The Linking Priority Stack
Not all internal links are equal. From most to least powerful:
1. **In-content links** — within the body copy of a relevant page. Most natural, most powerful.
2. **Hub page links** — the pillar page linking to all its spokes. High equity because pillar pages are linked from everywhere.
3. **Navigation links** — sitewide, consistent, but diluted by their ubiquity.
4. **Footer links** — sitewide, but Google gives them less weight than in-content.
5. **Sidebar links** — OK but often not in the main content flow.
See `references/internal-linking-playbook.md` for patterns and scripts.
---
## Common Architecture Mistakes
| Mistake | Why It Hurts | Fix |
|---------|-------------|-----|
| Orphan pages | No equity flows in, Google deprioritizes | Add contextual internal links from related content |
| URL changes without redirects | Inbound links become broken, equity lost | Always 301 redirect old URLs to new ones |
| Duplicate paths | `/blog/seo` and `/resources/seo` covering same topic | Consolidate with canonical or merge content |
| Deep nesting (4+ levels) | Crawl equity diluted, users confused | Flatten structure, remove unnecessary directories |
| Sitewide footer links to every post | Footer equity is diluted across 500 links | Footer should link to high-value pages only |
| Navigation that doesn't match user intent | Users leave, rankings drop | Run card-sort tests — let users show you their mental model |
| Homepage linking nowhere | Home is highest-equity page — use it | Link from home to key hub pages |
| Category pages with no content | Thin pages rank poorly | Add content to all hub/category pages |
| Dynamic URLs with parameters | `?sort=&filter=` creates duplicate content | Canonicalize or block with robots.txt |
---
## Proactive Triggers
Surface these without being asked:
- **Pages more than 3 clicks from homepage** → flag as crawl equity risk. Any page a user has to click 4+ times to reach needs a structural shortcut.
- **Category/hub page has thin or no content** → hub pages without real content don't rank. Flag and recommend adding a proper pillar page.
- **Internal links using generic anchor text ("click here", "read more")** → wasted signal. Offer to rewrite anchor text patterns.
- **No breadcrumbs on deep pages** → missing upward equity links and BreadcrumbList schema opportunity.
- **Sitemap includes noindex pages** → sitemap should only contain pages you want indexed. Flag and offer to filter.
- **Primary nav links to utility pages (contact, privacy)** → pushing equity to low-value pages. Nav should prioritize money/content pages.
---
## Output Artifacts
| When you ask for... | You get... |
|---------------------|------------|
| Architecture audit | Structural scorecard: depth distribution, orphan count, URL pattern issues, navigation gaps + prioritized fix list |
| New site structure | Text-based site tree (hierarchy diagram) + URL spec table with notes per section |
| Internal linking plan | Hub-and-spoke map per topic cluster + anchor text guidelines + orphan fix list |
| URL redesign | Before/after URL table + 301 redirect mapping + implementation checklist |
| Silo strategy | Topic cluster map per business goal + content gap analysis + pillar page brief |
---
## Communication
All output follows the structured communication standard:
- **Bottom line first** — answer before explanation
- **What + Why + How** — every finding has all three
- **Actions have owners and deadlines** — no "we should consider"
- **Confidence tagging** — 🟢 verified / 🟡 medium / 🔴 assumed
---
## Related Skills
- **seo-audit**: For comprehensive SEO audit covering technical, on-page, and off-page. Use seo-audit when architecture is one of several problem areas. NOT for deep structural redesign — use site-architecture.
- **schema-markup**: For structured data implementation. Use after site-architecture when you want to add BreadcrumbList and other schema to your finalized structure.
- **content-strategy**: For deciding what content to create. Use content-strategy to plan the content, then site-architecture to determine where it lives and how it links.
- **programmatic-seo**: When you need to generate hundreds or thousands of pages systematically. Site-architecture provides the URL and structural patterns that programmatic-seo scales.
- **seo-audit**: For identifying technical issues. NOT for architecture redesign planning — use site-architecture for that.
FILE:references/internal-linking-playbook.md
# Internal Linking Playbook
Patterns for building an internal link structure that distributes equity intelligently and reinforces topical authority.
---
## The Three Goals of Internal Linking
1. **Crawlability** — every page should be reachable from the homepage in 3 clicks or fewer
2. **Equity flow** — link equity flows from authoritative pages to pages you want to rank
3. **Topical signals** — anchor text and link context tell Google what a page is about
Most sites get none of these right. The ones that do compound their SEO advantage over time.
---
## Linking Architecture Patterns
### Pattern 1: Hub-and-Spoke (Topic Cluster)
Best for: Content sites, blogs, SaaS feature/solution pages.
```
Hub (Pillar) Page
├── Spoke 1 (Sub-topic)
│ └── Deep 1a (Specific guide within sub-topic)
│ └── Deep 1b
├── Spoke 2 (Sub-topic)
│ └── Deep 2a
└── Spoke 3 (Sub-topic)
```
**Link rules:**
- Hub → all spokes (contextual, in-body links)
- Each spoke → hub (with anchor text matching hub's target keyword)
- Each spoke → adjacent spokes (only when genuinely relevant)
- Deep pages → parent spoke + hub
**What makes this work:** The hub becomes the authority page because it receives links from everything in the cluster. Google sees a well-linked hub as the definitive resource on the topic.
---
### Pattern 2: Linear (Sequential Content)
Best for: Course content, multi-part guides, documentation, step-by-step processes.
```
Introduction → Part 1 → Part 2 → Part 3 → Summary/CTA
```
**Link rules:**
- Each page links forward (next) and back (previous)
- An index page links to all parts
- Summary page links back to each key section
**What makes this work:** Clear navigation for users, clear sequence for crawlers.
---
### Pattern 3: Conversion Funnel Linking
Best for: SaaS sites, lead gen sites — moving users from content to conversion.
```
Blog Post (awareness) → Feature Page (consideration) → Pricing Page (decision)
Blog Post (awareness) → Case Study (social proof) → Free Trial / Demo CTA
```
**Link rules:**
- Every blog post should have at least one contextual link to a product/feature page
- Case studies link to the relevant feature/solution and to pricing
- Feature pages link to relevant case studies and to pricing
- Pricing page links to FAQ and to demo/trial
**What makes this work:** Equity flows from content (high link volume) to money pages (low link volume). Most SaaS sites have this backwards — money pages get links from the nav only.
---
### Pattern 4: Star / Authority Distribution
Best for: Homepage and top-level hub pages that have lots of external links.
```
Homepage (authority source)
├── Service Page A (direct link from homepage)
├── Feature Page B (direct link from homepage)
├── Blog Category Hub (direct link from homepage)
└── Case Studies Hub (direct link from homepage)
```
**Link rules:**
- Homepage links only to the most important pages
- Not to every blog post — to the category hubs
- Each hub then distributes equity downward
**What makes this work:** Homepage equity isn't diluted across 200 blog links. It concentrates on 5-8 priority pages, which then funnel it to their children.
---
## Anchor Text Strategy
### The Right Mix
| Type | Target % of Internal Links | Example |
|------|--------------------------|---------|
| Descriptive partial match | 50-60% | "cold email writing guide" |
| Exact match keyword | 10-15% | "cold email templates" |
| Page title / branded | 20-25% | "our guide to cold outreach" |
| Generic | <5% | "learn more" |
| Naked URL | 0% | Never |
### Writing Good Anchor Text
**Good:** Uses the target keyword naturally in a sentence.
> "For tactical patterns, see our [cold email frameworks](link)."
**Bad:** Forces exact match where it sounds unnatural.
> "Click here to read our cold email templates cold email cold outreach guide."
**Bad:** Generic — signals nothing.
> "For more information, [click here](link)."
### Anchor Text Diversification
Don't link to the same page with the same anchor every time. Vary it. If you have 15 internal links to your "cold email templates" page:
- 8 using variations: "email outreach templates," "cold outreach scripts," "first-email frameworks"
- 4 using exact: "cold email templates"
- 3 using title/branded: "our template library"
This looks natural and covers a wider keyword base.
---
## Finding Linking Opportunities
### Method 1: Keyword Overlap Search (Manual)
When you publish new content, search your site for pages that mention the topic but don't link to the new page.
```
site:yourdomain.com "cold email"
```
Any page that mentions "cold email" and doesn't already link to your cold email guide is a candidate for adding a contextual link.
### Method 2: Screaming Frog Crawl
Crawl your site with Screaming Frog → Bulk Export → Internal links. Then filter:
- Pages with 0 inbound internal links = orphans (fix immediately)
- Pages with 1-2 inbound internal links = at-risk (add more)
- Pages with high outbound links but low inbound = over-givers (these should be receiving, not just giving)
### Method 3: Content Gap Linking
When you audit your content clusters, look for spokes that aren't linked from the hub. The hub should explicitly link to every key spoke page. If it doesn't, the cluster is broken.
---
## Orphan Page Recovery
An orphan page has no internal links pointing to it. It's effectively invisible to Google's link graph.
**Step 1: Find your orphans**
- Run `scripts/sitemap_analyzer.py` to get all indexed URLs
- Cross-reference with your internal link graph (from Screaming Frog or GSC)
- Pages in sitemap but not in internal link graph = candidates
**Step 2: Classify them**
| Type | Action |
|------|--------|
| Valuable content, no home | Find existing relevant pages to add contextual links from; add to relevant hub |
| Landing pages (PPC, events) | These are intentionally unlinked — check if they're accidentally indexed |
| Duplicate / thin content | Consolidate with canonical or noindex |
| Old content no longer relevant | Consider 301 redirect to updated version or 410 |
**Step 3: Fix in priority order**
1. Orphans with inbound external links first (equity is flowing in but going nowhere)
2. Orphans with good content and search potential
3. Orphans with thin content (fix content first, then link)
---
## Internal Link Audit Checklist
Run this quarterly:
- [ ] Every key page is reachable in ≤3 clicks from homepage
- [ ] Pillar/hub pages have links from all their spokes
- [ ] All spoke pages link back to their hub
- [ ] No orphan pages (pages with zero internal inbound links)
- [ ] Homepage links to 5-8 priority sections only
- [ ] Footer links limited to high-value pages (10-15 max)
- [ ] New content published in the last 30 days has at least 3 contextual inbound internal links
- [ ] No broken internal links (404s from internal sources)
- [ ] Anchor text is descriptive, not generic
- [ ] Pages with highest external backlinks are linking to money/conversion pages
---
## Common Patterns That Fail
### The Footer Dump
Putting 80 links in the footer because "they should be accessible." Google gives footer links minimal weight and won't thank you for linking to every blog post from there. Footer = navigation to key sections + legal. That's it.
### The "Related Posts" Widget Approach Only
Auto-generated related posts widgets are fine as supplemental linking, but they don't replace intentional contextual linking. The widget links to "related" content by tag or category — not necessarily to what you actually want to rank. Do the manual work.
### The Nav-Only Money Pages
Feature pages and pricing pages that only appear in the navigation get equity from nav links only. Powerful nav links are sitewide — but adding 5-10 contextual blog links to your pricing page is a significant equity boost. Write one blog post that organically links to pricing. That's real.
### Linking to Pages You Want to Rank for the Wrong Topic
If your /blog/seo-guide has 30 internal links to it but all the anchor text says "our guide" and "learn more," you're not sending a topical signal. The link equity flows in, but Google doesn't know what topic to attribute. Fix anchor text.
### Never Touching Old Posts
Old blog posts accumulate internal links over time because new posts link to them. But they rarely link out to newer, better content. When you publish new content, go back and update old posts to add contextual links to the new piece. This is one of the highest-ROI activities in content SEO.
FILE:references/url-design-guide.md
# URL Design Guide
URL structure by site type — with examples of what good and bad looks like in practice.
---
## Universal URL Rules
Before the site-specific patterns, these apply everywhere:
1. **Lowercase always** — `/Blog/SEO-Tips` and `/blog/seo-tips` are different URLs. Always lowercase.
2. **Hyphens, not underscores** — Google treats hyphens as word separators. Underscores join words. `/seo-tips` not `/seo_tips`.
3. **No special characters** — No `%`, `&`, `#`, `?` in the path itself.
4. **No trailing slash inconsistency** — Pick a convention (`/page` or `/page/`) and enforce it sitewide with redirects.
5. **No dates in URLs unless required** — `/blog/2024/03/seo-tips` ages poorly. `/blog/seo-tips` is evergreen.
6. **Stop words are usually fine** — `/how-to-write-cold-emails` is readable and fine. Don't obsessively remove "how", "to", "a", "the" unless the URL is very long.
7. **Keep them short** — Under 75 characters is a good target. Shorter is usually better.
---
## SaaS / B2B Software
### Recommended Structure
```
/ (homepage)
/features
/features/[feature-name] e.g. /features/email-automation
/pricing
/solutions/[use-case] e.g. /solutions/sales-teams
/solutions/[industry] e.g. /solutions/healthcare
/integrations
/integrations/[tool-name] e.g. /integrations/salesforce
/blog
/blog/[post-slug] e.g. /blog/cold-email-templates
/customers
/customers/[customer-name] e.g. /customers/acme-corp
/about
/changelog
/docs (or subdomain: docs.example.com)
/docs/[topic]/[subtopic]
```
### What Works and What Doesn't
| ✅ Do | ❌ Don't |
|-------|----------|
| `/pricing` | `/pricing-plans` (redundant) |
| `/features/email-automation` | `/product/features/email-automation/detail` (too deep) |
| `/blog/cold-email-guide` | `/blog/articles/cold-email/complete-guide-to-cold-email` (too long) |
| `/solutions/sales-teams` | `/solutions-for-sales-teams` (ugly) |
| `/integrations/hubspot` | `/connect-with/hubspot-integration` |
### SaaS-Specific Notes
- `/features/` pages should actually be rankable landing pages, not just nav items.
- `/solutions/` by use case captures bottom-funnel searches ("sales team email tool").
- `/integrations/[tool]` pages are high-intent SEO goldmines — build a real page for each.
- Blog posts should live at `/blog/[slug]` — not `/resources/`, not `/learn/`, not `/content/`. Pick one.
- Changelog belongs at `/changelog` — some companies put it at `/releases` or `/updates`. Fine, just pick one.
---
## Blog / Content Site
### Recommended Structure
```
/ (homepage)
/[category] e.g. /seo, /email-marketing, /content
/[category]/[post-slug] e.g. /seo/technical-seo-audit-checklist
/guides (optional hub for long-form guides)
/guides/[guide-slug] e.g. /guides/cold-email-complete-guide
/tools (optional if you have free tools)
/tools/[tool-name]
/author/[author-slug]
/tag/[tag-name] (often better to noindex tags)
```
### What Works and What Doesn't
| ✅ Do | ❌ Don't |
|-------|----------|
| `/seo/keyword-research-guide` | `/seo/keyword-research/a-complete-guide-to-keyword-research-for-beginners-in-2024` |
| `/guides/cold-email` | `/blog/2024/03/15/cold-email-guide` |
| `/author/reza-rezvani` | `/author?id=42` |
| Flat category → post structure | 4-level nesting |
### Blog-Specific Notes
- Date-based URLs (`/2024/03/15/slug`) age poorly and look stale. Avoid.
- Tag pages create duplicate/thin content at scale. Either noindex them or give them real content.
- If you have <500 posts, flat `/post-slug` is fine. If you have >500, category buckets help organization.
- Author pages are worth building as real pages — they help E-E-A-T signals.
---
## E-Commerce
### Recommended Structure
```
/ (homepage)
/collections (or /shop, /catalog)
/collections/[category] e.g. /collections/mens-shoes
/collections/[category]/[subcategory] e.g. /collections/mens-shoes/running
/products/[product-slug] e.g. /products/air-max-270-black
/brands/[brand-name]
/sale
/new-arrivals
/blog
/blog/[post-slug]
```
### What Works and What Doesn't
| ✅ Do | ❌ Don't |
|-------|----------|
| `/products/air-max-270-black` | `/products?id=89472&color=black&size=10` |
| `/collections/mens-shoes` | `/products/shoes/men/athletic/running/all-styles` |
| Canonical on variant pages | Let `?color=red&size=10` create duplicate URLs |
### E-Commerce-Specific Notes
- Product variant pages (size, color) are the biggest duplicate content risk in e-commerce. Use canonical tags pointing to the base product URL, or use URL parameters and configure them in GSC.
- Filter and sort pages (`?sort=price-asc&brand=nike`) should either be canonicalized or blocked.
- Collection/category pages need real content to rank — not just a product grid.
- Discontinued products: don't just delete them. 301 to closest alternative or return 410 with a helpful message.
---
## Local Business / Service Area
### Recommended Structure (Single Location)
```
/ (homepage)
/services
/services/[service-name] e.g. /services/plumbing-repair
/about
/contact
/blog
/blog/[post-slug]
/areas-served (optional hub for service area pages)
/areas-served/[city-name] e.g. /areas-served/brooklyn
```
### Recommended Structure (Multi-Location)
```
/ (homepage)
/locations
/locations/[city] e.g. /locations/new-york
/locations/[city]/[service] e.g. /locations/new-york/plumbing
/services/[service-name] (generic service pages)
```
### Local-Specific Notes
- City/location pages must have unique, locally relevant content — not just "Find our [service] in [city]" copy-pasted 47 times.
- `/areas-served/brooklyn` should have real information about serving Brooklyn, not a thin page.
- Multi-location sites: `/locations/[city]` works better than subdomain per city for smaller operations. Subdomains make sense for truly independent franchises.
---
## URL Redirect Mapping (When Restructuring)
If you're changing URLs, you need a 301 redirect map. Every old URL → new URL. No exceptions.
**Redirect mapping process:**
1. Export all indexed URLs from Google Search Console (Crawl → Coverage → All)
2. Export all inbound links to your site (use Ahrefs, Semrush, or GSC)
3. Map old → new for every URL that has inbound links or search traffic
4. Implement 301 redirects server-side (not JS redirects, not meta refresh)
5. Monitor in GSC for 404 errors after migration
6. Update internal links — don't just redirect, fix the source links
**Priority redirect tiers:**
- **Tier 1:** Pages with significant inbound external links — redirect these first
- **Tier 2:** Pages with significant organic traffic — redirect to preserve equity
- **Tier 3:** Pages with neither — still redirect, but lower urgency
**Never:**
- Chain redirects more than 1 hop (`/old` → `/temp` → `/new` wastes equity)
- 302 redirect something that's a permanent move (use 301)
- Leave old URLs live as duplicates without canonicals
---
## Canonicalization
When the same content is accessible at multiple URLs, tell Google which one is canonical.
```html
<link rel="canonical" href="https://example.com/the-canonical-url" />
```
Common scenarios requiring canonicals:
- `http://` vs `https://` — canonical should always be `https://`
- `www` vs non-www — pick one, canonical + 301 the other
- Trailing slash vs no trailing slash — `/page` and `/page/` are different URLs to Google
- Filtered/sorted product pages — canonical to base product/collection URL
- Paginated pages — canonical the first page (or use `rel=next`/`rel=prev`)
- Printer-friendly versions — canonical to main page
- Syndicated content — canonical to original source
---
## HTTP Status Code Reference
| Code | Meaning | Use |
|------|---------|-----|
| 200 | OK | Normal page |
| 301 | Moved Permanently | URL changed permanently — passes equity |
| 302 | Found (Temporary) | Temporary redirect — does NOT pass equity |
| 404 | Not Found | Page doesn't exist — configure a helpful 404 page |
| 410 | Gone | Page intentionally removed — Google deindexes faster than 404 |
| 503 | Service Unavailable | Maintenance mode — tell Google to come back later |
Use 301, not 302, for all permanent URL changes.
FILE:scripts/sitemap_analyzer.py
#!/usr/bin/env python3
"""
sitemap_analyzer.py — Analyzes sitemap.xml files for structure, depth, and potential issues.
Usage:
python3 sitemap_analyzer.py [sitemap.xml]
python3 sitemap_analyzer.py https://example.com/sitemap.xml (fetches via urllib)
cat sitemap.xml | python3 sitemap_analyzer.py
If no file is provided, runs on embedded sample sitemap for demonstration.
Output: Structural analysis with depth distribution, URL patterns, orphan candidates,
duplicate path detection, and JSON summary.
Stdlib only — no external dependencies.
"""
import json
import sys
import re
import select
import urllib.request
import urllib.error
from collections import Counter, defaultdict
from urllib.parse import urlparse
import xml.etree.ElementTree as ET
# ─── Namespaces used in sitemaps ─────────────────────────────────────────────
SITEMAP_NAMESPACES = {
"sm": "http://www.sitemaps.org/schemas/sitemap/0.9",
"image": "http://www.google.com/schemas/sitemap-image/1.1",
"video": "http://www.google.com/schemas/sitemap-video/1.1",
"news": "http://www.google.com/schemas/sitemap-news/0.9",
"xhtml": "http://www.w3.org/1999/xhtml",
}
# ─── Sample sitemap (embedded) ────────────────────────────────────────────────
SAMPLE_SITEMAP = """<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<!-- Homepage -->
<url>
<loc>https://example.com/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<!-- Top-level pages -->
<url><loc>https://example.com/pricing</loc></url>
<url><loc>https://example.com/about</loc></url>
<url><loc>https://example.com/contact</loc></url>
<url><loc>https://example.com/blog</loc></url>
<!-- Features section -->
<url><loc>https://example.com/features</loc></url>
<url><loc>https://example.com/features/email-automation</loc></url>
<url><loc>https://example.com/features/crm-integration</loc></url>
<url><loc>https://example.com/features/analytics</loc></url>
<!-- Solutions section -->
<url><loc>https://example.com/solutions/sales-teams</loc></url>
<url><loc>https://example.com/solutions/marketing-teams</loc></url>
<!-- Blog posts (various topics) -->
<url><loc>https://example.com/blog/cold-email-guide</loc></url>
<url><loc>https://example.com/blog/email-open-rates</loc></url>
<url><loc>https://example.com/blog/crm-comparison</loc></url>
<url><loc>https://example.com/blog/sales-process-optimization</loc></url>
<!-- Deeply nested pages (potential issue) -->
<url><loc>https://example.com/resources/guides/email/cold-outreach/advanced/templates</loc></url>
<url><loc>https://example.com/resources/guides/email/cold-outreach/advanced/scripts</loc></url>
<!-- Duplicate path patterns (potential issue) -->
<url><loc>https://example.com/blog/email-tips</loc></url>
<url><loc>https://example.com/resources/email-tips</loc></url>
<!-- Dynamic-looking URL (potential issue) -->
<url><loc>https://example.com/search?q=cold+email&sort=recent</loc></url>
<!-- Case studies -->
<url><loc>https://example.com/customers/acme-corp</loc></url>
<url><loc>https://example.com/customers/globex</loc></url>
<!-- Legal pages (often over-linked) -->
<url><loc>https://example.com/privacy</loc></url>
<url><loc>https://example.com/terms</loc></url>
</urlset>
"""
# ─── URL Analysis ─────────────────────────────────────────────────────────────
def get_depth(path: str) -> int:
"""Return depth of a URL path. / = 0, /blog = 1, /blog/post = 2, etc."""
parts = [p for p in path.strip("/").split("/") if p]
return len(parts)
def get_path_pattern(path: str) -> str:
"""Replace variable segments with {slug} for pattern detection."""
parts = path.strip("/").split("/")
normalized = []
for p in parts:
if p:
# Keep static segments (likely structure), replace dynamic-looking ones
if re.match(r'^[a-z][-a-z]+$', p) and len(p) < 30:
normalized.append(p)
else:
normalized.append("{slug}")
return "/" + "/".join(normalized) if normalized else "/"
def has_query_params(url: str) -> bool:
return "?" in url
def looks_like_dynamic_url(url: str) -> bool:
parsed = urlparse(url)
return bool(parsed.query)
def detect_path_siblings(urls: list) -> list:
"""Find URLs with same slug in different parent directories (potential duplicates)."""
slug_to_paths = defaultdict(list)
for url in urls:
path = urlparse(url).path.strip("/")
slug = path.split("/")[-1] if path else ""
if slug:
slug_to_paths[slug].append(url)
duplicates = []
for slug, paths in slug_to_paths.items():
if len(paths) > 1:
# Only flag if they're in different directories
parents = set("/".join(urlparse(p).path.strip("/").split("/")[:-1]) for p in paths)
if len(parents) > 1:
duplicates.append({"slug": slug, "urls": paths})
return duplicates
# ─── Sitemap Parser ──────────────────────────────────────────────────────────
def parse_sitemap(content: str) -> list:
"""Parse sitemap XML and return list of URL dicts."""
urls = []
# Strip namespace declarations for simpler parsing
content_clean = re.sub(r'xmlns[^=]*="[^"]*"', '', content)
try:
root = ET.fromstring(content_clean)
except ET.ParseError as e:
print(f"❌ XML parse error: {e}", file=sys.stderr)
return []
# Handle sitemap index (points to other sitemaps)
if root.tag.endswith("sitemapindex") or root.tag == "sitemapindex":
print("ℹ️ This is a sitemap index file — it points to child sitemaps.")
print(" Child sitemaps:")
for sitemap in root.findall(".//{http://www.sitemaps.org/schemas/sitemap/0.9}loc") or root.findall(".//loc"):
print(f" - {sitemap.text}")
print(" Run this tool on each child sitemap for full analysis.")
return []
# Regular urlset
for url_el in root.findall(".//{http://www.sitemaps.org/schemas/sitemap/0.9}url") or root.findall(".//url"):
loc_el = url_el.find("{http://www.sitemaps.org/schemas/sitemap/0.9}loc") or url_el.find("loc")
lastmod_el = url_el.find("{http://www.sitemaps.org/schemas/sitemap/0.9}lastmod") or url_el.find("lastmod")
priority_el = url_el.find("{http://www.sitemaps.org/schemas/sitemap/0.9}priority") or url_el.find("priority")
if loc_el is not None and loc_el.text:
urls.append({
"url": loc_el.text.strip(),
"lastmod": lastmod_el.text.strip() if lastmod_el is not None and lastmod_el.text else None,
"priority": float(priority_el.text.strip()) if priority_el is not None and priority_el.text else None,
})
return urls
# ─── Analysis Engine ─────────────────────────────────────────────────────────
def analyze_urls(urls: list) -> dict:
raw_urls = [u["url"] for u in urls]
paths = [urlparse(u).path for u in raw_urls]
depths = [get_depth(p) for p in paths]
depth_counter = Counter(depths)
dynamic_urls = [u for u in raw_urls if looks_like_dynamic_url(u)]
patterns = Counter(get_path_pattern(urlparse(u).path) for u in raw_urls)
top_patterns = patterns.most_common(10)
duplicate_slugs = detect_path_siblings(raw_urls)
deep_urls = [(u, get_depth(urlparse(u).path)) for u in raw_urls if get_depth(urlparse(u).path) >= 4]
# Extract top-level directories
top_dirs = Counter()
for p in paths:
parts = p.strip("/").split("/")
if parts and parts[0]:
top_dirs[parts[0]] += 1
return {
"total_urls": len(urls),
"depth_distribution": dict(sorted(depth_counter.items())),
"top_directories": dict(top_dirs.most_common(15)),
"dynamic_urls": dynamic_urls,
"deep_pages": deep_urls,
"duplicate_slug_candidates": duplicate_slugs,
"top_url_patterns": [{"pattern": p, "count": c} for p, c in top_patterns],
}
# ─── Report Printer ──────────────────────────────────────────────────────────
def grade_depth_distribution(dist: dict) -> str:
deep = sum(v for k, v in dist.items() if k >= 4)
total = sum(dist.values())
if total == 0:
return "N/A"
pct = deep / total * 100
if pct < 5:
return "🟢 Excellent"
if pct < 15:
return "🟡 Acceptable"
return "🔴 Too many deep pages"
def print_report(analysis: dict) -> None:
print("\n" + "═" * 62)
print(" SITEMAP STRUCTURE ANALYSIS")
print("═" * 62)
print(f"\n Total URLs: {analysis['total_urls']}")
print("\n── Depth Distribution ──")
dist = analysis["depth_distribution"]
total = analysis["total_urls"]
for depth, count in sorted(dist.items()):
pct = count / total * 100 if total else 0
bar = "█" * int(pct / 2)
label = "homepage" if depth == 0 else f"{' ' * min(depth, 3)}/{'…/' * (depth - 1)}page"
print(f" Depth {depth}: {count:4d} pages ({pct:5.1f}%) {bar} {label}")
print(f"\n Rating: {grade_depth_distribution(dist)}")
deep_pct = sum(v for k, v in dist.items() if k >= 4) / total * 100 if total else 0
if deep_pct >= 5:
print(" ⚠️ More than 5% of pages are 4+ levels deep.")
print(" Consider flattening structure or adding shortcut links.")
print("\n── Top-Level Directories ──")
for d, count in analysis["top_directories"].items():
pct = count / total * 100 if total else 0
print(f" /{d:<30s} {count:4d} URLs ({pct:.1f}%)")
print("\n── URL Pattern Analysis ──")
for p in analysis["top_url_patterns"]:
print(f" {p['pattern']:<45s} {p['count']:4d} URLs")
if analysis["dynamic_urls"]:
print(f"\n── Dynamic URLs Detected ({len(analysis['dynamic_urls'])}) ──")
print(" ⚠️ URLs with query parameters should usually be excluded from sitemap.")
print(" Use canonical tags or robots.txt to prevent duplicate content indexing.")
for u in analysis["dynamic_urls"][:5]:
print(f" {u}")
if len(analysis["dynamic_urls"]) > 5:
print(f" ... and {len(analysis['dynamic_urls']) - 5} more")
if analysis["deep_pages"]:
print(f"\n── Deep Pages (4+ Levels) ({len(analysis['deep_pages'])}) ──")
print(" ⚠️ Pages this deep may have weak crawl equity. Add internal shortcuts.")
for url, depth in analysis["deep_pages"][:5]:
print(f" Depth {depth}: {url}")
if len(analysis["deep_pages"]) > 5:
print(f" ... and {len(analysis['deep_pages']) - 5} more")
if analysis["duplicate_slug_candidates"]:
print(f"\n── Potential Duplicate Path Issues ({len(analysis['duplicate_slug_candidates'])}) ──")
print(" ⚠️ Same slug appears in multiple directories — possible duplicate content.")
for item in analysis["duplicate_slug_candidates"][:5]:
print(f" Slug: '{item['slug']}'")
for u in item["urls"]:
print(f" - {u}")
if len(analysis["duplicate_slug_candidates"]) > 5:
print(f" ... and {len(analysis['duplicate_slug_candidates']) - 5} more")
print("\n── Recommendations ──")
has_issues = False
if analysis["dynamic_urls"]:
print(" 1. Remove dynamic URLs (with ?) from sitemap.")
has_issues = True
if analysis["deep_pages"]:
print(f" {'2' if has_issues else '1'}. Flatten deep URL structures or add internal shortcut links.")
has_issues = True
if analysis["duplicate_slug_candidates"]:
print(f" {'3' if has_issues else '1'}. Review duplicate slug paths — consolidate or add canonical tags.")
has_issues = True
if not has_issues:
print(" ✅ No major structural issues detected in this sitemap.")
print("\n" + "═" * 62)
# ─── Main ─────────────────────────────────────────────────────────────────────
def load_content(source: str) -> str:
"""Load sitemap from file path, URL, or stdin."""
if source.startswith("http://") or source.startswith("https://"):
try:
with urllib.request.urlopen(source, timeout=10) as resp:
return resp.read().decode("utf-8")
except urllib.error.URLError as e:
print(f"Error fetching URL: {e}", file=sys.stderr)
sys.exit(1)
else:
try:
with open(source, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"Error: File not found: {source}", file=sys.stderr)
sys.exit(1)
def main():
import argparse
parser = argparse.ArgumentParser(
description="Analyzes sitemap.xml files for structure, depth, and potential issues. "
"Reports depth distribution, URL patterns, orphan candidates, and duplicates."
)
parser.add_argument(
"file", nargs="?", default=None,
help="Path to a sitemap.xml file or URL (https://...). "
"Use '-' to read from stdin. If omitted, runs embedded sample."
)
args = parser.parse_args()
if args.file:
if args.file == "-":
content = sys.stdin.read()
else:
content = load_content(args.file)
else:
print("No file or URL provided — running on embedded sample sitemap.\n")
content = SAMPLE_SITEMAP
urls = parse_sitemap(content)
if not urls:
print("No URLs found in sitemap.", file=sys.stderr)
sys.exit(1)
analysis = analyze_urls(urls)
print_report(analysis)
# JSON output
print("\n── JSON Summary ──")
summary = {
"total_urls": analysis["total_urls"],
"depth_distribution": analysis["depth_distribution"],
"dynamic_url_count": len(analysis["dynamic_urls"]),
"deep_page_count": len(analysis["deep_pages"]),
"duplicate_slug_count": len(analysis["duplicate_slug_candidates"]),
"top_directories": analysis["top_directories"],
}
print(json.dumps(summary, indent=2))
if __name__ == "__main__":
main()
When the user wants to optimize signup, registration, account creation, or trial activation flows. Also use when the user mentions "signup conversions," "reg...
---
name: "signup-flow-cro"
description: When the user wants to optimize signup, registration, account creation, or trial activation flows. Also use when the user mentions "signup conversions," "registration friction," "signup form optimization," "free trial signup," "reduce signup dropoff," or "account creation flow." For post-signup onboarding, see onboarding-cro. For lead capture forms (not account creation), see form-cro.
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Signup Flow CRO
You are an expert in optimizing signup and registration flows. Your goal is to reduce friction, increase completion rates, and set users up for successful activation.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before providing recommendations, understand:
1. **Flow Type**
- Free trial signup
- Freemium account creation
- Paid account creation
- Waitlist/early access signup
- B2B vs B2C
2. **Current State**
- How many steps/screens?
- What fields are required?
- What's the current completion rate?
- Where do users drop off?
3. **Business Constraints**
- What data is genuinely needed at signup?
- Are there compliance requirements?
- What happens immediately after signup?
---
## Core Principles
→ See references/signup-cro-playbook.md for details
## Output Format
### Audit Findings
For each issue found:
- **Issue**: What's wrong
- **Impact**: Why it matters (with estimated impact if possible)
- **Fix**: Specific recommendation
- **Priority**: High/Medium/Low
### Recommended Changes
Organized by:
1. Quick wins (same-day fixes)
2. High-impact changes (week-level effort)
3. Test hypotheses (things to A/B test)
### Form Redesign (if requested)
- Recommended field set with rationale
- Field order
- Copy for labels, placeholders, buttons, errors
- Visual layout suggestions
---
## Common Signup Flow Patterns
### B2B SaaS Trial
1. Email + Password (or Google auth)
2. Name + Company (optional: role)
3. → Onboarding flow
### B2C App
1. Google/Apple auth OR Email
2. → Product experience
3. Profile completion later
### Waitlist/Early Access
1. Email only
2. Optional: Role/use case question
3. → Waitlist confirmation
### E-commerce Account
1. Guest checkout as default
2. Account creation optional post-purchase
3. OR Social auth with single click
---
## Experiment Ideas
### Form Design Experiments
**Layout & Structure**
- Single-step vs. multi-step signup flow
- Multi-step with progress bar vs. without
- 1-column vs. 2-column field layout
- Form embedded on page vs. separate signup page
- Horizontal vs. vertical field alignment
**Field Optimization**
- Reduce to minimum fields (email + password only)
- Add or remove phone number field
- Single "Name" field vs. "First/Last" split
- Add or remove company/organization field
- Test required vs. optional field balance
**Authentication Options**
- Add SSO options (Google, Microsoft, GitHub, LinkedIn)
- SSO prominent vs. email form prominent
- Test which SSO options resonate (varies by audience)
- SSO-only vs. SSO + email option
**Visual Design**
- Test button colors and sizes for CTA prominence
- Plain background vs. product-related visuals
- Test form container styling (card vs. minimal)
- Mobile-optimized layout testing
---
### Copy & Messaging Experiments
**Headlines & CTAs**
- Test headline variations above signup form
- CTA button text: "Create Account" vs. "Start Free Trial" vs. "Get Started"
- Add clarity around trial length in CTA
- Test value proposition emphasis in form header
**Microcopy**
- Field labels: minimal vs. descriptive
- Placeholder text optimization
- Error message clarity and tone
- Password requirement display (upfront vs. on error)
**Trust Elements**
- Add social proof next to signup form
- Test trust badges near form (security, compliance)
- Add "No credit card required" messaging
- Include privacy assurance copy
---
### Trial & Commitment Experiments
**Free Trial Variations**
- Credit card required vs. not required for trial
- Test trial length impact (7 vs. 14 vs. 30 days)
- Freemium vs. free trial model
- Trial with limited features vs. full access
**Friction Points**
- Email verification required vs. delayed vs. removed
- Test CAPTCHA impact on completion
- Terms acceptance checkbox vs. implicit acceptance
- Phone verification for high-value accounts
---
### Post-Submit Experiments
- Clear next steps messaging after signup
- Instant product access vs. email confirmation first
- Personalized welcome message based on signup data
- Auto-login after signup vs. require login
---
## Task-Specific Questions
1. What's your current signup completion rate?
2. Do you have field-level analytics on drop-off?
3. What data is absolutely required before they can use the product?
4. Are there compliance or verification requirements?
5. What happens immediately after signup?
---
## Related Skills
- **onboarding-cro** — WHEN: the signup flow itself completes well but users aren't activating or reaching their "aha moment" after account creation. WHEN NOT: don't jump to onboarding-cro when users are dropping off during the signup form itself.
- **form-cro** — WHEN: the form being optimized is NOT account creation — lead capture, contact, demo request, or survey forms need form-cro instead. WHEN NOT: don't use form-cro for registration/account creation flows; signup-flow-cro has the right framework for authentication patterns (SSO, magic link, email+password).
- **page-cro** — WHEN: the landing page or marketing page leading to the signup is the bottleneck — poor headline, weak value prop, or message mismatch. WHEN NOT: don't invoke page-cro when users are reaching the signup form but dropping inside it.
- **ab-test-setup** — WHEN: hypotheses from the signup audit are ready to test (SSO vs. email, single-step vs. multi-step, credit card required vs. not). WHEN NOT: don't run A/B tests on the signup flow before instrumenting field-level drop-off analytics.
- **paywall-upgrade-cro** — WHEN: the signup flow is freemium and the real challenge is converting free users to paid, not getting them to sign up. WHEN NOT: don't conflate trial-to-paid conversion with signup-flow optimization.
- **marketing-context** — WHEN: check `.claude/product-marketing-context.md` for B2B vs. B2C context, compliance requirements, and qualification data needs before designing the field set. WHEN NOT: skip if user has provided explicit product and compliance context in the conversation.
---
## Communication
All signup flow CRO output follows this quality standard:
- Recommendations are always organized as **Quick Wins → High-Impact → Test Hypotheses** — never a flat list
- Every field removal recommendation is justified against the "do we need this before they can use the product?" test
- SSO options are always considered and recommended when relevant — don't default to email-only flows
- Post-submit experience (verification, success state, next steps) is always addressed — it's part of the flow
- Mobile optimization is treated as a distinct section, not an afterthought
- Experiment ideas distinguish between "fix this" (obvious) and "test this" (uncertain) — never recommend testing obvious improvements
---
## Proactive Triggers
Automatically surface signup-flow-cro when:
1. **"Users sign up but don't activate"** — Low activation rate often traces back to signup friction or a broken post-submit experience; proactively audit the full signup-to-activation path.
2. **"Our trial conversion is low"** — When the trial-to-paid rate is poor, check whether the signup flow is setting wrong expectations or collecting the wrong users.
3. **Free trial or freemium product being built** — When product or engineering work on a new trial flow is detected, proactively offer signup-flow-cro review before launch.
4. **"Should we require a credit card?"** — This question always triggers the full signup friction analysis and trial commitment experiment framework.
5. **High mobile drop-off on signup** — When analytics or page-cro reveals a mobile gap specifically on the signup page, immediately surface the mobile signup optimization checklist.
---
## Output Artifacts
| Artifact | Format | Description |
|----------|--------|-------------|
| Signup Flow Audit | Issue/Impact/Fix/Priority table | Per-step and per-field analysis with severity ratings |
| Recommended Field Set | Justified list | Required vs. deferrable fields with rationale, organized by signup step |
| Flow Redesign Spec | Step-by-step outline | Recommended multi-step or single-step flow with copy for each screen |
| SSO & Auth Options Recommendation | Decision table | Which auth methods to offer, placement, and priority for the target audience |
| A/B Test Hypotheses | Table | Hypothesis × variant description × success metric × priority for top 3-5 tests |
FILE:references/signup-cro-playbook.md
# signup-flow-cro reference
## Core Principles
### 1. Minimize Required Fields
Every field reduces conversion. For each field, ask:
- Do we absolutely need this before they can use the product?
- Can we collect this later through progressive profiling?
- Can we infer this from other data?
**Typical field priority:**
- Essential: Email (or phone), Password
- Often needed: Name
- Usually deferrable: Company, Role, Team size, Phone, Address
### 2. Show Value Before Asking for Commitment
- What can you show/give before requiring signup?
- Can they experience the product before creating an account?
- Reverse the order: value first, signup second
### 3. Reduce Perceived Effort
- Show progress if multi-step
- Group related fields
- Use smart defaults
- Pre-fill when possible
### 4. Remove Uncertainty
- Clear expectations ("Takes 30 seconds")
- Show what happens after signup
- No surprises (hidden requirements, unexpected steps)
---
## Field-by-Field Optimization
### Email Field
- Single field (no email confirmation field)
- Inline validation for format
- Check for common typos (gmial.com → gmail.com)
- Clear error messages
### Password Field
- Show password toggle (eye icon)
- Show requirements upfront, not after failure
- Consider passphrase hints for strength
- Update requirement indicators in real-time
**Better password UX:**
- Allow paste (don't disable)
- Show strength meter instead of rigid rules
- Consider passwordless options
### Name Field
- Single "Full name" field vs. First/Last split (test this)
- Only require if immediately used (personalization)
- Consider making optional
### Social Auth Options
- Place prominently (often higher conversion than email)
- Show most relevant options for your audience
- B2C: Google, Apple, Facebook
- B2B: Google, Microsoft, SSO
- Clear visual separation from email signup
- Consider "Sign up with Google" as primary
### Phone Number
- Defer unless essential (SMS verification, calling leads)
- If required, explain why
- Use proper input type with country code handling
- Format as they type
### Company/Organization
- Defer if possible
- Auto-suggest as they type
- Infer from email domain when possible
### Use Case / Role Questions
- Defer to onboarding if possible
- If needed at signup, keep to one question
- Use progressive disclosure (don't show all options at once)
---
## Single-Step vs. Multi-Step
### Single-Step Works When:
- 3 or fewer fields
- Simple B2C products
- High-intent visitors (from ads, waitlist)
### Multi-Step Works When:
- More than 3-4 fields needed
- Complex B2B products needing segmentation
- You need to collect different types of info
### Multi-Step Best Practices
- Show progress indicator
- Lead with easy questions (name, email)
- Put harder questions later (after psychological commitment)
- Each step should feel completable in seconds
- Allow back navigation
- Save progress (don't lose data on refresh)
**Progressive commitment pattern:**
1. Email only (lowest barrier)
2. Password + name
3. Customization questions (optional)
---
## Trust and Friction Reduction
### At the Form Level
- "No credit card required" (if true)
- "Free forever" or "14-day free trial"
- Privacy note: "We'll never share your email"
- Security badges if relevant
- Testimonial near signup form
### Error Handling
- Inline validation (not just on submit)
- Specific error messages ("Email already registered" + recovery path)
- Don't clear the form on error
- Focus on the problem field
### Microcopy
- Placeholder text: Use for examples, not labels
- Labels: Always visible (not just placeholders)
- Help text: Only when needed, placed close to field
---
## Mobile Signup Optimization
- Larger touch targets (44px+ height)
- Appropriate keyboard types (email, tel, etc.)
- Autofill support
- Reduce typing (social auth, pre-fill)
- Single column layout
- Sticky CTA button
- Test with actual devices
---
## Post-Submit Experience
### Success State
- Clear confirmation
- Immediate next step
- If email verification required:
- Explain what to do
- Easy resend option
- Check spam reminder
- Option to change email if wrong
### Verification Flows
- Consider delaying verification until necessary
- Magic link as alternative to password
- Let users explore while awaiting verification
- Clear re-engagement if verification stalls
---
## Measurement
### Key Metrics
- Form start rate (landed → started filling)
- Form completion rate (started → submitted)
- Field-level drop-off (which fields lose people)
- Time to complete
- Error rate by field
- Mobile vs. desktop completion
### What to Track
- Each field interaction (focus, blur, error)
- Step progression in multi-step
- Social auth vs. email signup ratio
- Time between steps
---
FILE:scripts/funnel_drop_analyzer.py
#!/usr/bin/env python3
"""
funnel_drop_analyzer.py — Signup Funnel Drop-Off Analyzer
100% stdlib, no pip installs required.
Usage:
python3 funnel_drop_analyzer.py # demo mode
python3 funnel_drop_analyzer.py --steps steps.json
python3 funnel_drop_analyzer.py --steps steps.json --json
echo '[{"step":"Visit","count":10000}]' | python3 funnel_drop_analyzer.py --stdin
steps.json format:
[
{"step": "Landing Page Visit", "count": 10000},
{"step": "Clicked Sign Up", "count": 4200},
{"step": "Filled Form", "count": 2800},
{"step": "Email Verified", "count": 1900},
{"step": "Onboarding Done", "count": 1100}
]
"""
import argparse
import json
import math
import sys
# ---------------------------------------------------------------------------
# Recommendation engine
# ---------------------------------------------------------------------------
RECOMMENDATIONS = {
"high_drop": {
"threshold": 0.50, # >50% drop
"landing_page": [
"Value proposition may be unclear — run a 5-second test.",
"Add social proof (testimonials, logos, user count) above the fold.",
"Ensure CTA button is prominent and benefit-focused ('Start Free' not 'Submit').",
],
"clicked_sign_up": [
"CTA label or placement may not resonate — A/B test button copy and colour.",
"Users may not trust the product — add trust badges and reviews near CTA.",
"Consider a sticky header CTA for long landing pages.",
],
"filled_form": [
"Form has too many fields — reduce to email + password minimum.",
"Try progressive disclosure: collect extra info post-signup.",
"Add inline validation so errors appear in real-time, not on submit.",
"Show a progress indicator if multi-step.",
],
"email_verified": [
"Verification email may land in spam — check SPF/DKIM/DMARC.",
"Send a plain-text follow-up 30 min after signup nudging verification.",
"Consider SMS or magic-link alternatives to email verification.",
"Reduce time-to-value: show a useful screen before requiring verification.",
],
"default": [
"Significant drop detected — instrument with session recordings (Hotjar/FullStory).",
"Run exit surveys at this step to capture qualitative reasons.",
"Check for UI bugs or broken flows on mobile.",
],
},
"medium_drop": {
"threshold": 0.25, # 25–50% drop
"default": [
"Moderate friction — review copy and UX at this step.",
"Ensure mobile experience is frictionless (test on real devices).",
"Add micro-copy explaining why information is requested.",
],
},
"healthy": {
"default": [
"Step conversion is healthy — focus optimisation effort elsewhere.",
],
},
}
def classify_step_name(name: str) -> str:
"""Map step name to a known category for targeted recommendations."""
n = name.lower()
if any(k in n for k in ["land", "visit", "page", "home"]):
return "landing_page"
if any(k in n for k in ["cta", "click", "signup", "sign up", "register", "start"]):
return "clicked_sign_up"
if any(k in n for k in ["form", "fill", "detail", "info", "enter"]):
return "filled_form"
if any(k in n for k in ["email", "verif", "confirm", "activate"]):
return "email_verified"
return "default"
def get_recommendation(step_name: str, drop_rate: float) -> list:
if drop_rate > RECOMMENDATIONS["high_drop"]["threshold"]:
bucket = RECOMMENDATIONS["high_drop"]
cat = classify_step_name(step_name)
return bucket.get(cat, bucket["default"])
elif drop_rate > RECOMMENDATIONS["medium_drop"]["threshold"]:
return RECOMMENDATIONS["medium_drop"]["default"]
else:
return RECOMMENDATIONS["healthy"]["default"]
# ---------------------------------------------------------------------------
# Core analysis
# ---------------------------------------------------------------------------
def analyze_funnel(steps: list) -> dict:
"""
Analyse a funnel step list and return full metrics + recommendations.
Each step: {"step": <str>, "count": <int>}
"""
if not steps:
raise ValueError("steps list is empty")
if len(steps) < 2:
raise ValueError("Need at least 2 steps to analyse a funnel")
top_count = steps[0]["count"]
if top_count <= 0:
raise ValueError("Top-of-funnel count must be > 0")
step_metrics = []
worst_step = None
worst_drop_rate = -1.0
for i, s in enumerate(steps):
name = s["step"]
count = s["count"]
cumulative_rate = count / top_count
if i == 0:
step_to_step_rate = 1.0
drop_count = 0
drop_rate = 0.0
recommendations = ["Top of funnel — all visitors enter here."]
else:
prev_count = steps[i - 1]["count"]
step_to_step_rate = count / prev_count if prev_count > 0 else 0.0
drop_count = prev_count - count
drop_rate = 1 - step_to_step_rate
recommendations = get_recommendation(name, drop_rate)
if drop_rate > worst_drop_rate:
worst_drop_rate = drop_rate
worst_step = name
step_metrics.append({
"step": name,
"count": count,
"step_conversion_pct": round(step_to_step_rate * 100, 2),
"step_drop_pct": round(drop_rate * 100, 2),
"drop_count": drop_count,
"cumulative_conversion_pct": round(cumulative_rate * 100, 2),
"recommendations": recommendations,
})
# Overall funnel health score (0-100)
overall_conv = steps[-1]["count"] / top_count
score = _funnel_score(step_metrics, overall_conv)
return {
"summary": {
"total_steps": len(steps),
"top_of_funnel_count": top_count,
"bottom_of_funnel_count": steps[-1]["count"],
"overall_conversion_pct": round(overall_conv * 100, 2),
"worst_performing_step": worst_step,
"worst_step_drop_pct": round(worst_drop_rate * 100, 2),
"funnel_health_score": score,
"funnel_health_label": _score_label(score),
},
"steps": step_metrics,
"top_priority": _top_priority(step_metrics),
}
def _funnel_score(step_metrics: list, overall_conv: float) -> int:
"""
Score = 100 * overall_conversion adjusted for worst-step severity.
- Base: log-scale overall conversion (capped at a 10% target = 100 pts)
- Penalty: each step with >60% drop deducts points
"""
target_conv = 0.10 # 10% overall = score 100
base = min(100, math.log1p(overall_conv) / math.log1p(target_conv) * 100)
penalty = 0
for m in step_metrics[1:]:
if m["step_drop_pct"] > 60:
penalty += 10
elif m["step_drop_pct"] > 40:
penalty += 5
score = max(0, round(base - penalty))
return score
def _score_label(s: int) -> str:
if s >= 80: return "Excellent"
if s >= 60: return "Good"
if s >= 40: return "Fair"
if s >= 20: return "Poor"
return "Critical"
def _top_priority(step_metrics: list) -> dict:
"""Return the single highest-impact step to fix first."""
# Pick step with largest absolute drop count (not just rate)
candidates = step_metrics[1:]
if not candidates:
return {}
top = max(candidates, key=lambda m: m["drop_count"])
return {
"step": top["step"],
"drop_count": top["drop_count"],
"drop_pct": top["step_drop_pct"],
"why": "Largest absolute visitor loss — highest revenue impact.",
"quick_wins": top["recommendations"],
}
# ---------------------------------------------------------------------------
# Pretty-print
# ---------------------------------------------------------------------------
def pretty_print(result: dict) -> None:
s = result["summary"]
tp = result["top_priority"]
print("\n" + "=" * 65)
print(" SIGNUP FUNNEL DROP-OFF ANALYZER")
print("=" * 65)
print(f"\n📊 FUNNEL OVERVIEW")
print(f" Top of funnel : {s['top_of_funnel_count']:,} visitors")
print(f" Bottom of funnel : {s['bottom_of_funnel_count']:,} converted")
print(f" Overall conversion : {s['overall_conversion_pct']}%")
print(f" Funnel health : {s['funnel_health_score']}/100 ({s['funnel_health_label']})")
print(f" Worst step : {s['worst_performing_step']} "
f"({s['worst_step_drop_pct']}% drop)")
print(f"\n{'Step':<28} {'Count':>8} {'Step Conv':>10} {'Step Drop':>10} {'Cumul Conv':>10}")
print("─" * 75)
for m in result["steps"]:
bar = "█" * int(m["cumulative_conversion_pct"] / 5)
print(f" {m['step']:<26} {m['count']:>8,} "
f"{m['step_conversion_pct']:>9.1f}% "
f"{m['step_drop_pct']:>9.1f}% "
f"{m['cumulative_conversion_pct']:>9.1f}% {bar}")
print(f"\n🚨 TOP PRIORITY FIX: {tp.get('step', 'N/A')}")
print(f" Lost visitors : {tp.get('drop_count', 0):,} ({tp.get('drop_pct', 0)}% drop)")
print(f" Why fix first : {tp.get('why', '')}")
print(" Quick wins:")
for qw in tp.get("quick_wins", []):
print(f" • {qw}")
print(f"\n💡 STEP-BY-STEP RECOMMENDATIONS")
for m in result["steps"][1:]:
if m["step_drop_pct"] > 10:
print(f"\n [{m['step']}] ↓{m['step_drop_pct']}% drop")
for r in m["recommendations"]:
print(f" • {r}")
print()
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
DEMO_STEPS = [
{"step": "Landing Page Visit", "count": 12000},
{"step": "Clicked Sign Up CTA", "count": 4560},
{"step": "Filled Registration", "count": 2800},
{"step": "Email Verified", "count": 1540},
{"step": "Onboarding Completed", "count": 880},
{"step": "First Core Action", "count": 420},
]
def parse_args():
parser = argparse.ArgumentParser(
description="Analyse signup funnel drop-off by step (stdlib only).",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--steps", type=str, default=None,
help="Path to JSON file with funnel steps")
parser.add_argument("--stdin", action="store_true",
help="Read steps JSON from stdin")
parser.add_argument("--json", action="store_true",
help="Output results as JSON")
return parser.parse_args()
def main():
args = parse_args()
steps = None
if args.stdin:
steps = json.load(sys.stdin)
elif args.steps:
with open(args.steps) as f:
steps = json.load(f)
else:
print("🔬 DEMO MODE — using sample SaaS signup funnel\n")
steps = DEMO_STEPS
result = analyze_funnel(steps)
if args.json:
print(json.dumps(result, indent=2))
else:
pretty_print(result)
if __name__ == "__main__":
main()
When the user wants to design, launch, or optimize a referral or affiliate program. Use when they mention 'referral program,' 'affiliate program,' 'word of m...
---
name: "referral-program"
description: "When the user wants to design, launch, or optimize a referral or affiliate program. Use when they mention 'referral program,' 'affiliate program,' 'word of mouth,' 'refer a friend,' 'incentive program,' 'customer referrals,' 'brand ambassador,' 'partner program,' 'referral link,' or 'growth through referrals.' Covers program mechanics, incentive design, and optimization — not just the idea of referrals but the actual system."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Referral Program
You are a growth engineer who has designed referral and affiliate programs for SaaS companies, marketplaces, and consumer apps. You know the difference between programs that compound and programs that collect dust. Your goal is to build a referral system that actually runs — one with the right mechanics, triggers, incentives, and measurement to make customers do your acquisition for you.
## Before Starting
**Check for context first:**
If `marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered.
Gather this context (ask if not provided):
### 1. Product & Customer
- What are you selling? (SaaS, marketplace, service, ecommerce)
- Who is your ideal customer and what do they love about your product?
- What's your average LTV? (This determines incentive ceiling)
- What's your current CAC via other channels?
### 2. Program Goals
- What outcome do you want? (More signups, more revenue, brand reach)
- Is this B2C or B2B? (Different mechanics apply)
- Do you want customers referring customers, or partners promoting your product?
### 3. Current State (if optimizing)
- What program exists today?
- What are the key metrics? (Referral rate, conversion rate, active referrers %)
- What's the reward structure?
- Where does the loop break down?
---
## How This Skill Works
### Mode 1: Design a New Program
Starting from scratch. Build the full referral program — loop, incentives, triggers, and measurement.
**Workflow:**
1. Define the referral loop (4 stages)
2. Choose program type (customer referral vs. affiliate)
3. Design the incentive structure (what, when, for whom)
4. Identify trigger moments (when to ask for referrals)
5. Plan the share mechanics (how referrals actually happen)
6. Define measurement framework
### Mode 2: Optimize an Existing Program
You have something running but it's underperforming. Diagnose where the loop breaks.
**Workflow:**
1. Audit current metrics against benchmarks
2. Identify the specific weak point (low awareness, low share rate, low conversion, reward friction)
3. Run a focused fix — don't redesign everything at once
4. Measure the impact before moving to the next lever
### Mode 3: Launch an Affiliate Program
Different from customer referrals. Affiliates are external promoters — bloggers, influencers, complementary SaaS, industry newsletters — motivated by commission, not loyalty.
**Workflow:**
1. Define affiliate tiers and commission structure
2. Identify and recruit initial affiliate partners
3. Build the affiliate toolkit (links, assets, copy)
4. Set tracking and payout mechanics
5. Onboard and activate your first 10 affiliates
---
## Referral vs. Affiliate — Choose the Right Mechanism
| | Customer Referral | Affiliate Program |
|---|---|---|
| **Who promotes** | Your existing customers | External partners, publishers, influencers |
| **Motivation** | Loyalty, reward, social currency | Commission, audience alignment |
| **Best for** | B2C, prosumer, SMB SaaS | B2B SaaS, high LTV products, content-heavy niches |
| **Activation** | Triggered by aha moment, milestone | Recruited proactively, onboarded |
| **Payout timing** | Account credit, discount, cash reward | Revenue share or flat fee per conversion |
| **CAC impact** | Low — reward < CAC | Variable — commission % determines |
| **Scale** | Scales with user base | Scales with partner recruitment |
**Rule of thumb:** If your customers are enthusiastic and social, start with customer referrals. If your customers are businesses buying on behalf of a team, start with affiliates.
---
## The Referral Loop
Every referral program runs on the same 4-stage loop. If any stage is weak, the loop breaks.
```
[Trigger Moment] → [Share Action] → [Referred User Converts] → [Reward Delivered] → [Loop]
```
### Stage 1: Trigger Moment
This is when you ask customers to refer. Timing is everything.
**High-signal trigger moments:**
- **After aha moment** — when the customer first experiences core value (not at signup — too early)
- **After a milestone** — "You just saved your 100th hour" / "Your 10th team member joined"
- **After great support** — post-resolution NPS prompt → if 9-10, ask for referral
- **After renewal** — customers who renew are telling you they're satisfied
- **After a public win** — customer tweets about you → follow up with referral link
**What doesn't work:** Asking on day 1, asking in onboarding emails, asking in the footer of every email.
### Stage 2: Share Action
Remove every possible point of friction.
- Pre-filled share message (editable, not locked)
- Personal referral link (not a generic coupon code)
- Share options: email invite, link copy, social share, Slack/Teams share for B2B
- Mobile-optimized for consumer products
- One-click send — no manual copy-paste required
### Stage 3: Referred User Converts
The referred user lands on your product. Now what?
- Personalized landing ("Your friend Alex invited you — here's your bonus...")
- Incentive visible on landing page
- Referral attribution tracked from landing to conversion
- Clear CTA — don't make them hunt for what to do
### Stage 4: Reward Delivered
Reward must be fast and clear. Delayed rewards break the loop.
- Confirm reward eligibility as soon as referral signs up (not when they pay)
- Notify the referrer immediately — don't wait until month-end
- Status visible in dashboard ("2 friends joined — you've earned $40")
---
## Incentive Design
### Single-Sided vs. Double-Sided
**Single-sided** (referrer only gets rewarded): Use when your product has strong viral hooks and customers are already enthusiastic. Lower cost per referral.
**Double-sided** (both referrer and referred get rewarded): Use when you need to overcome inertia on both sides. Higher cost, higher conversion. Dropbox made this famous.
**Rule:** If your referral rate is <1%, go double-sided. If it's >5%, single-sided is more profitable.
### Reward Types
| Type | Best For | Examples |
|------|----------|---------|
| Account credit | SaaS / subscription | "Get $20 credit" |
| Discount | Ecommerce / usage-based | "Get 1 month free" |
| Cash | High LTV, B2C | "$50 per referral" |
| Feature unlock | Freemium | "Unlock advanced analytics" |
| Status / recognition | Community / loyalty | "Ambassador status, exclusive badge" |
| Charity donation | Enterprise / mission-driven | "$25 to a cause you choose" |
**Sizing rule:** Reward should be ≥10% of first month's value for account credit. For cash, cap at 30% of first payment. Run `scripts/referral_roi_calculator.py` to model reward sizing against your LTV and CAC.
### Tiered Rewards (Gamification)
When you want referrers to go from 1 referral to 10:
```
1 referral → $20 credit
3 referrals → $75 credit (25/referral) + bonus feature
10 referrals → $300 cash + ambassador status
```
Keep tiers simple. Three levels maximum. Each tier should feel meaningfully better, not just slightly better.
---
## Optimization Levers
Don't optimize randomly. Diagnose first, then pull the right lever.
| Metric | Benchmark | If Below Benchmark |
|--------|-----------|-------------------|
| Referral program awareness | >40% of active users know it exists | Promote in-app, post-activation emails |
| Active referrers (%) | 5–15% of active user base | Improve trigger moments and visibility |
| Referral share rate | 20–40% of those who see it share | Simplify share flow, improve messaging |
| Referred conversion rate | 15–25% (vs. 5-10% organic) | Improve referred landing page, add incentive |
| Reward redemption rate | >70% within 30 days | Reduce friction, send reminders |
### Improving Referral Rate
- Move the trigger moment earlier (after aha, not after 90 days)
- Add referral prompt to success states ("You just hit 1,000 contacts — share this with a colleague?")
- Surface the program in the product dashboard, not just in emails
- Test double-sided vs. single-sided rewards
### Improving Referred User Conversion
- Personalize the landing page ("Invited by [Name]")
- Show the referred user their specific benefit above the fold
- Reduce signup friction — if they're referred, they're warm; don't make them jump through hoops
- A/B test the referral landing page like a paid traffic landing page
---
## Key Metrics
Track these weekly:
| Metric | Formula | Why It Matters |
|--------|---------|----------------|
| Referral rate | Referrals sent / active users | Health of the program |
| Active referrers % | Users who sent ≥1 referral / total active users | Engagement depth |
| Referral conversion rate | Referrals that converted / referrals sent | Quality of referred traffic |
| CAC via referral | Reward cost / new customers via referral | Program economics vs. other channels |
| Referral revenue contribution | Revenue from referred customers / total revenue | Business impact |
| Virality coefficient (K) | Referrals per user × conversion rate | K >1 = viral growth |
See [references/measurement-framework.md](references/measurement-framework.md) for benchmarks by industry and optimization playbook.
---
## Affiliate Program Launch Checklist
If launching an affiliate program specifically:
**Before Launch**
- [ ] Commission structure defined (% of revenue or flat fee per conversion)
- [ ] Cookie window set (30 days minimum, 90 days for B2B)
- [ ] Affiliate tracking platform selected (Impact, ShareASale, Rewardful, PartnerStack, or custom)
- [ ] Affiliate agreement drafted (legal review recommended)
- [ ] Payment terms clear (threshold, frequency, method)
**Partner Toolkit**
- [ ] Unique tracking links for each affiliate
- [ ] Pre-written copy and email swipes
- [ ] Approved images and banner ads
- [ ] Product explanation sheet (what to tell their audience)
- [ ] Landing page optimized for affiliate traffic
**Recruitment**
- [ ] List of 50 target affiliates (complementary SaaS, newsletters, bloggers, agencies)
- [ ] Personalized outreach — not a generic "join our affiliate program" email
- [ ] 10-affiliate pilot before scaling
See [references/program-mechanics.md](references/program-mechanics.md) for detailed program patterns and real-world examples.
---
## Proactive Triggers
Surface these without being asked:
- **Asking at signup** → Flag immediately. Asking a new user to refer before they've experienced value is a conversion killer. Move trigger to post-aha moment.
- **Reward too small relative to LTV** → If reward is <5% of LTV and referral rate is low, the math is broken. Surface the sizing issue.
- **No reward notification system** → If referred users convert but referrers aren't notified immediately, the loop breaks. Flag the need for instant notification.
- **Generic share message** → Pre-filled messages that sound like marketing copy get deleted. Flag and rewrite in first-person customer voice.
- **No attribution after the landing page** → If referral tracking stops at first visit but conversion requires multiple sessions, referral is being undercounted. Flag tracking gap.
- **Affiliate program without a partner kit** → If affiliates don't have approved copy and assets, they'll promote inaccurately or not at all. Flag before launch.
---
## Output Artifacts
| When you ask for... | You get... |
|---------------------|------------|
| "Design a referral program" | Full program spec: loop design, incentive structure, trigger moments, share mechanics, measurement plan |
| "Audit our referral program" | Metric scorecard vs. benchmarks, weak link diagnosis, prioritized optimization plan |
| "Model our incentive options" | ROI comparison of 3-5 reward structures using your LTV and CAC data |
| "Write referral program copy" | In-app prompts, referral email, referred user landing page headline, share messages |
| "Launch an affiliate program" | Launch checklist, commission structure recommendation, partner recruitment list template, affiliate kit outline |
| "What should our K-factor be?" | Virality model with your numbers — current K, target K, what needs to change to get there |
---
## Communication
All output follows the structured communication standard:
- **Bottom line first** — answer before explanation
- **Numbers-grounded** — every recommendation tied to your LTV/CAC inputs
- **Confidence tagging** — 🟢 verified / 🟡 medium / 🔴 assumed
- **Actions have owners** — "define reward structure" → assign an owner and timeline
---
## Related Skills
- **launch-strategy**: Use when planning the go-to-market for a product launch. NOT for building a referral program (different mechanics, different timeline).
- **email-sequence**: Use when building the email flow that supports the referral program (trigger emails, reward notifications). NOT for the program design itself.
- **marketing-demand-acquisition**: Use for multi-channel paid and organic acquisition strategy. NOT for referral-specific mechanics.
- **ab-test-setup**: Use when A/B testing referral landing pages, reward structures, or trigger messaging. NOT for the program design.
- **content-creator**: Use for creating affiliate partner content or referral-related blog posts. NOT for program mechanics.
FILE:references/measurement-framework.md
# Measurement Framework — Referral Program Metrics, Benchmarks, and Optimization Playbook
The metrics that tell you if your referral program is working, what's broken, and what to fix first.
---
## The Core Metric Stack
Track these weekly. Everything else is secondary.
| Metric | Formula | Benchmark (SaaS) | What It Tells You |
|--------|---------|-----------------|------------------|
| Program awareness | (Users who know about program / Total active users) × 100 | >40% | Are you even promoting it? |
| Active referrer rate | (Users who sent ≥1 referral / Total active users) × 100 | 5–15% | How many users are actually participating |
| Referrals sent per active referrer | Total referrals / Active referrers | 2–5 per period | How motivated referrers are |
| Referral conversion rate | (Referrals that converted / Referrals sent) × 100 | 15–30% | Quality of referred traffic |
| Reward redemption rate | (Rewards redeemed / Rewards issued) × 100 | >70% | Is the reward actually desirable? |
| CAC via referral | Total reward cost / New customers via referral | <50% of channel CAC | Program efficiency |
| K-factor (virality coefficient) | Referrals per user × Referral conversion rate | >0.5 for meaningful growth | Is it self-sustaining? |
---
## Benchmarks by Stage and Model
### Early-Stage SaaS (<$1M ARR)
| Metric | Expected | Strong |
|--------|---------|--------|
| Active referrer rate | 2–5% | >8% |
| Referral conversion rate | 10–20% | >25% |
| CAC via referral vs. paid | 30–50% of paid CAC | <25% of paid CAC |
### Growth-Stage SaaS ($1M–$10M ARR)
| Metric | Expected | Strong |
|--------|---------|--------|
| Active referrer rate | 5–10% | >12% |
| Referral contribution to new signups | 10–20% | >25% |
| Referral contribution to revenue | 5–15% | >20% |
### Consumer / Prosumer Products
| Metric | Expected | Strong |
|--------|---------|--------|
| Active referrer rate | 8–20% | >25% |
| Referral conversion rate | 20–40% | >50% (with double-sided reward) |
| K-factor | 0.3–0.7 | >1.0 (true viral loop) |
### B2B Mid-Market (ACV $10k+)
| Metric | Expected | Strong |
|--------|---------|--------|
| Active referrer rate | 3–8% | >10% |
| Referral conversion rate | 20–40% (warm intros convert higher) | >50% |
| Average deal size via referral vs. standard | Similar | 20–40% higher (trust shortens negotiation) |
---
## Diagnosing the Broken Stage
### Diagnosis Framework
```
Is referral rate low?
└── Is awareness low? → Promote the program
└── Is trigger placement wrong? → Move to better moment
└── Is reward insufficient? → Test higher reward
└── Is share flow too complex? → Simplify
Is referral conversion low?
└── Is the landing page cold? → Personalize for referred users
└── Is the incentive for the referred user unclear? → Make it above the fold
└── Is signup friction high? → Reduce required fields
Is reward redemption low?
└── Is reward notification delayed? → Send immediately on qualifying event
└── Is reward type wrong? → Test cash vs. credit vs. feature unlock
└── Is the redemption process complex? → Auto-apply credits, remove steps
```
---
## The Optimization Playbook
Work in this order. Don't try to fix everything at once.
### Phase 1: Foundation (Month 1)
**Goal:** Get to baseline awareness and share rate.
1. Audit whether users know the program exists
2. Add in-app promotion: dashboard banner, post-activation prompt, success state trigger
3. Add referral program to the weekly/monthly activation email
4. Ensure share flow works on mobile
**Success gate:** Program awareness >30%, Active referrer rate >3%
### Phase 2: Trigger Optimization (Month 2)
**Goal:** Ask at the right moment, not just any moment.
1. Map all current trigger points
2. Move or add trigger to first aha moment (define aha moment first)
3. A/B test: trigger after aha vs. trigger after 7-day retention
4. Add NPS-linked trigger: score of 9-10 → immediate referral ask
**Success gate:** Active referrer rate increases by 30% over Phase 1
### Phase 3: Incentive Tuning (Month 3)
**Goal:** Right reward, right timing, right delivery.
1. Survey churned referrers — why did they stop?
2. Test single-sided vs. double-sided if not already tested
3. Test reward type: credit vs. cash vs. feature unlock
4. Add reward status widget to dashboard: "You've earned $X. [View details]"
5. Reduce reward payout delay — reward immediately on qualifying event, not month-end
**Success gate:** Reward redemption rate >70%, CAC via referral <40% of paid CAC
### Phase 4: Conversion of Referred Users (Month 4)
**Goal:** Referred users should convert at 2× organic rate.
1. Personalize referred user landing page (use referrer name if available)
2. Highlight referred user's incentive above the fold — don't bury it
3. A/B test: direct to product vs. direct to dedicated referral landing page
4. Add "referred by" onboarding track: faster to aha, lower time to first value
**Success gate:** Referred user conversion rate 20%+ (vs. organic baseline)
### Phase 5: Scale and Gamification (Month 5+)
**Goal:** Turn your top 5% of referrers into a real advocacy channel.
1. Identify top referrers — reach out personally
2. Offer top referrers early access, ambassador status, or product input role
3. Launch tiered reward structure
4. Quarterly referral challenges: "Top 10 referrers this quarter win X"
---
## CAC via Referral — Full Calculation
```
CAC via referral = (Reward cost per referral × Successful referrals) + Program overhead costs
───────────────────────────────────────────────────────────────────────────
New customers acquired via referral
Where:
- Reward cost per referral = referrer reward + referred user reward
- Program overhead = platform cost + engineering time + support time (amortized)
- Successful referrals = referrals that converted to paying customer
```
**Example:**
- 200 referrals sent → 40 conversions (20% conversion rate)
- Referrer reward: $30 per successful referral
- Referred user reward: $20 (discount on first month)
- Platform cost: $100/mo, engineering: $500/mo (amortized) → $600/mo overhead
- Program overhead per conversion: $600 / 40 = $15
**CAC via referral** = ($30 + $20) × 40 + $600 / 40 = **$65 per customer**
Compare to paid CAC, and you know if the program is worth it.
Use `scripts/referral_roi_calculator.py` to model this for your numbers.
---
## Affiliate-Specific Metrics
| Metric | Formula | Benchmark |
|--------|---------|-----------|
| Active affiliate rate | Active affiliates / Enrolled affiliates | 20–40% |
| Revenue per active affiliate | Total affiliate revenue / Active affiliates | Varies by niche |
| Affiliate-driven CAC | Commission paid / New customers via affiliate | Should be <standard CAC |
| Top affiliate concentration | Revenue from top 20% of affiliates | Normal if 80%+ of revenue from top 20% |
| Average cookie-to-conversion time | Days from click to first payment | Benchmark against your sales cycle |
**Warning sign:** If >80% of affiliate revenue is from 1–2 partners, you have concentration risk. One partner leaving could tank the channel overnight. Diversify proactively.
---
## Reporting Template
Weekly referral program summary:
```
REFERRAL PROGRAM — Week of [DATE]
Active referrers: X (↑/↓ vs. last week)
Referrals sent: X
Conversions: X (rate: X%)
Rewards issued: $X
New customers via referral: X
CAC via referral: $X (vs. $X paid CAC)
TOP THIS WEEK:
- [Name/segment] sent 12 referrals, 4 converted
- [Trigger optimization test] is showing +18% referrer rate
ISSUES:
- [What's broken and the plan to fix it]
NEXT ACTION:
- [One thing we're doing this week to improve the program]
```
FILE:references/program-mechanics.md
# Program Mechanics — Referral and Affiliate Design Patterns
Detailed design patterns with real-world examples. Use this as a reference when designing programs — these are the mechanics that separate programs with 10% referral rates from ones with 0.5%.
---
## The Two Fundamental Program Types
### Type A: Customer-to-Customer Referral
Your best customers refer their peers. Classic example: Dropbox, Airbnb, Uber.
**Core mechanics:**
- Referral link generated per user
- Reward given when referred user completes a qualifying action (sign up, first purchase, first month paid)
- Referrer sees their dashboard: links sent, signed up, rewards earned
**What makes it work:**
- Existing customer trust transfers. Being referred by someone you trust removes 80% of purchase skepticism.
- The referrer's reputation is on the line — they only refer people they think will benefit
- Natural social proof at the moment of conversion
### Type B: Partner / Affiliate Program
External publishers, influencers, agencies, or complementary SaaS tools promote you in exchange for commission.
**Core mechanics:**
- Unique affiliate link or coupon per partner
- Attribution tracked via cookie (30-90 day window typical)
- Payout on qualifying events (first payment, monthly recurring, flat fee)
**What makes it work:**
- Partners have existing audiences who trust them
- Content-driven promotion outlasts a single ad — a blog post with your affiliate link can generate leads for 3 years
- Commission-aligned incentives mean partners promote more when you convert better
---
## Real-World Program Patterns
### Pattern 1: Double-Sided Reward (Dropbox Model)
**How it worked:** Refer a friend = 500MB for you + 500MB for them.
**Why it worked:**
- Both sides had skin in the game
- The reward was intrinsic to the product (not a discount on something unrelated)
- The referred user's incentive made them more likely to complete registration
- Referrer felt generous, not transactional
**When to use:** When your core product has a natural "shareable" dimension. Digital products with quantity-based rewards (storage, credits, messages, seats) are perfect candidates.
**When NOT to use:** When your product has no natural unit to give. Don't give $10 Amazon gift cards just to copy Dropbox — tie the reward to product value.
---
### Pattern 2: Tiered Ambassador Program (Referral + Status)
**How it works:** Customers unlock higher reward tiers by referring more users. Top tier gets named ambassador status, exclusive access, or direct relationship with the company.
**Example structure:**
```
Bronze (1-2 referrals): $20 credit per referral
Silver (3-9 referrals): $30 credit per referral + priority support
Gold (10+ referrals): $50 credit per referral + product advisory board invite + named case study
```
**Why it works:** For highly enthusiastic customers, status beats cash. Naming someone an "ambassador" triggers identity — they become advocates rather than just referrers.
**When to use:** Strong community around the product. Developer tools, creative SaaS, agency tools where practitioners identify with the category.
---
### Pattern 3: Milestone Trigger (Conditional Reward)
**How it works:** Reward is not given at signup — it's given when the referred user reaches a specific milestone.
**Example:**
- "Your friend gets $50 when they make their first withdrawal"
- "You get 1 free month when your referral upgrades to a paid plan"
**Why it works:** Referred users are incentivized to actually use the product to unlock the reward. Referrers are incentivized to encourage their referral to stay active. Reduces reward fraud (fake accounts).
**When to use:** High-volume consumer products where gaming the system is a real risk. Financial services, marketplaces, usage-based products.
---
### Pattern 4: Cohort-Based Referral Window
**How it works:** Referral rewards expire if the referred user doesn't convert within a set window.
**Standard windows:**
- B2C: 7–14 days (high intent = fast decision)
- B2B SMB: 30 days
- B2B Enterprise: 90+ days (longer evaluation cycles)
**Why it matters:** Open-ended referral attribution creates accounting complexity and gaming risk. Time-bounded windows create urgency and clean accounting.
---
### Pattern 5: Affiliate Commission Tiers by Partner Type
Not all affiliates are equal. Tiering by partner type lets you reward your best partners appropriately.
**Example tier structure:**
```
Standard affiliates (bloggers, small newsletters):
└── 20% of first payment, 30-day cookie
Premium affiliates (high-traffic publications, active agencies):
└── 25% MRR for 12 months, 60-day cookie, co-marketing support
Strategic partners (complementary SaaS, resellers):
└── 30% MRR ongoing, white-label option, dedicated account manager
```
**Key principle:** The higher the traffic quality and deal size, the higher the commission can go. An agency that sends you 5 enterprise deals per year is worth more than 100 bloggers who send you occasional trials.
---
### Pattern 6: Product-Embedded Referral (Virality by Design)
The referral mechanism is built into the product experience, not bolted on as a "refer a friend" email.
**Examples:**
- Calendar invite: "Powered by [Product]" link in email footer that every invitee sees
- "Created with [Product]" watermark on exported documents (Canva, Notion)
- "Invite your team" prompt mid-onboarding with a clear reason to do it now
- "Share your results" on high-value output screens
**Why it works:** The referral happens at the moment of peak product value, using the product itself as the promotional vehicle. No separate "referral program" needed.
**When to use:** Productivity tools, creative tools, any product that produces shareable output. Build this alongside the product, not as an afterthought.
---
### Pattern 7: B2B Account-Based Referral
In B2B, referrals are more targeted — you're asking for warm intros to specific account types, not a spray-and-pray link share.
**How it works:**
- Identify which customers have the broadest networks in your ICP
- Equip them with a referral kit (email template, one-pager, LinkedIn intro script)
- Reward for completed intro + reward uplift for closed deal
- Keep the referrer informed on progress (increases likelihood of them championing internally)
**Example mechanics:**
```
Step 1: Customer completes an intro call → $200 gift card
Step 2: Intro converts to a demo → $500 additional
Step 3: Demo converts to a deal → 10% of first year's contract value (capped at $5,000)
```
**Why it works:** High-trust referrals from B2B customers often shorten sales cycles dramatically. The referrer becomes an internal champion at the referred company, not just a warm lead.
---
## Share Mechanics Deep Dive
### The 3 Share Channels That Drive Volume
| Channel | How It Works | Best For |
|---------|------------|---------|
| Personal referral link | User copies/shares their link to a friend | Universal |
| Direct email invite | User enters friend's email, platform sends invite on their behalf | Consumer, prosumer |
| Social share | One-click to Twitter, LinkedIn, WhatsApp with pre-filled message | Consumer, community products |
### Pre-Written Share Messages — What Works
**Works:**
> "I've been using [Product] for 3 months and it's saved me hours on [specific task]. You can get started free using my link: [link]"
**Doesn't work:**
> "Check out this amazing product I use! [link]"
The difference: specificity and personal endorsement. Pre-fill your share messages with the actual benefit, not generic praise. Make it easy for users to be specific advocates, not just sharers.
---
## Fraud Prevention
Referral fraud happens when users game the system (fake accounts, self-referrals, incentivized referrals).
**Minimum safeguards:**
- Email verification required before reward is credited
- Device fingerprinting to detect same-device self-referral
- Reward withheld until referred user completes a qualifying action (first payment, 7-day active use)
- Rate limiting on referral link sends per user
**Warning signs of fraud:**
- Referral conversion rate suddenly spikes above 60% (normal is 15–30%)
- High number of referrals from a single user (>20 in a week)
- Referrals with similar email patterns or same IP block
---
## Technology Options
### For Customer Referral Programs
| Tool | Best For | Pricing Tier |
|------|---------|-------------|
| ReferralHero | SMB SaaS, waitlist referral | $49–$199/mo |
| Viral Loops | Consumer apps, e-commerce | $49–$199/mo |
| Referral Rock | Mid-market SaaS | $175–$800/mo |
| Custom (in-house) | When you want full control + have engineering | Build cost only |
### For Affiliate Programs
| Tool | Best For | Notes |
|------|---------|-------|
| Rewardful | SaaS, Stripe-based | $49–$299/mo, easiest Stripe integration |
| PartnerStack | B2B SaaS | $500+/mo, best for partner tiers |
| Impact | Enterprise, multi-channel | Custom pricing |
| ShareASale | E-commerce, consumer | 20% of commissions + fees |
### For Product-Embedded Viral Loops
Build these in-house. The "powered by" footer, "created with" watermark, or "invite your team" prompt needs to be native to the product experience, not a third-party widget.
FILE:scripts/referral_roi_calculator.py
#!/usr/bin/env python3
"""
referral_roi_calculator.py — Calculates referral program ROI.
Models the economics of a referral program given your LTV, CAC, referral rate,
reward cost, and conversion rate. Outputs program ROI, break-even referral rate,
and optimal reward sizing.
Usage:
python3 referral_roi_calculator.py # runs embedded sample
python3 referral_roi_calculator.py params.json # uses your params
echo '{"ltv": 1200, "cac": 300}' | python3 referral_roi_calculator.py
JSON input format:
{
"ltv": 1200, # Customer Lifetime Value ($)
"cac": 300, # Current avg CAC via paid channels ($)
"active_users": 500, # Active users who could refer
"referral_rate": 0.05, # % of active users who refer each month (0.05 = 5%)
"referrals_per_referrer": 2.5, # Avg referrals sent per active referrer
"referral_conversion_rate": 0.20, # % of referrals who become customers
"referrer_reward": 50, # Reward paid to referrer per successful referral ($)
"referred_reward": 30, # Reward paid to referred user (0 if single-sided) ($)
"program_overhead_monthly": 200, # Platform + ops cost per month ($)
"churn_rate_monthly": 0.03, # Monthly churn rate (used for LTV validation)
"months_to_model": 12 # How many months to project
}
"""
import json
import sys
from collections import OrderedDict
# ---------------------------------------------------------------------------
# Core calculation functions
# ---------------------------------------------------------------------------
def calculate_referrals_per_month(params):
"""How many successful referrals per month?"""
active_users = params["active_users"]
referral_rate = params["referral_rate"]
referrals_per_referrer = params["referrals_per_referrer"]
conversion_rate = params["referral_conversion_rate"]
active_referrers = active_users * referral_rate
referrals_sent = active_referrers * referrals_per_referrer
conversions = referrals_sent * conversion_rate
return {
"active_referrers": round(active_referrers, 1),
"referrals_sent": round(referrals_sent, 1),
"new_customers_per_month": round(conversions, 1),
}
def calculate_monthly_program_cost(params, new_customers_per_month):
"""Total cost of running the program for one month."""
reward_per_conversion = params["referrer_reward"] + params["referred_reward"]
reward_cost = reward_per_conversion * new_customers_per_month
overhead = params["program_overhead_monthly"]
return {
"reward_cost": round(reward_cost, 2),
"overhead_cost": round(overhead, 2),
"total_cost": round(reward_cost + overhead, 2),
"reward_per_conversion": round(reward_per_conversion, 2),
}
def calculate_monthly_revenue(params, new_customers_per_month):
"""Revenue generated from referred customers in the first month."""
# First-month value is LTV / (1 / monthly_churn) = LTV * monthly_churn
# Simplified: use LTV * monthly_churn as first-month expected revenue contribution
# More conservative: just count as one acquisition with full LTV expected
ltv = params["ltv"]
revenue = new_customers_per_month * ltv
return round(revenue, 2)
def calculate_cac_via_referral(cost_data, new_customers_per_month):
if new_customers_per_month == 0:
return float('inf')
return round(cost_data["total_cost"] / new_customers_per_month, 2)
def calculate_break_even_referral_rate(params):
"""
What referral rate do we need so that CAC via referral equals
reward_per_conversion + overhead_per_customer_amortized?
We want: total_cost / new_customers = cac_target
Solving for referral_rate where cac_target = 50% of paid CAC (our target)
"""
target_cac = params["cac"] * 0.5 # goal: 50% of current CAC
ltv = params["ltv"]
active_users = params["active_users"]
referrals_per_referrer = params["referrals_per_referrer"]
conversion_rate = params["referral_conversion_rate"]
reward_per_conversion = params["referrer_reward"] + params["referred_reward"]
overhead = params["program_overhead_monthly"]
# CAC_referral = (reward × conversions + overhead) / conversions
# = reward + overhead/conversions
# Solve: target_cac = reward + overhead / (active_users × rate × referrals_per_referrer × conversion_rate)
# conversions_needed = overhead / (target_cac - reward)
if target_cac <= reward_per_conversion:
return None # impossible — reward alone exceeds target CAC
conversions_needed = overhead / (target_cac - reward_per_conversion)
referral_rate_needed = conversions_needed / (active_users * referrals_per_referrer * conversion_rate)
return round(referral_rate_needed, 4)
def calculate_optimal_reward(params):
"""
What's the maximum reward you can afford while keeping CAC via referral
under 60% of paid CAC?
max_total_reward = 0.60 × paid_CAC (using conversion-amortized overhead)
"""
target_cac = params["cac"] * 0.60
overhead_amortized = params["program_overhead_monthly"] / max(
calculate_referrals_per_month(params)["new_customers_per_month"], 1
)
max_reward = target_cac - overhead_amortized
# Split recommendation: 60% referrer, 40% referred (double-sided)
referrer_portion = round(max_reward * 0.60, 2)
referred_portion = round(max_reward * 0.40, 2)
return {
"max_total_reward": round(max(max_reward, 0), 2),
"recommended_referrer_reward": max(referrer_portion, 0),
"recommended_referred_reward": max(referred_portion, 0),
"reward_as_pct_ltv": round((max_reward / params["ltv"]) * 100, 1) if params["ltv"] > 0 else 0,
}
def calculate_roi(params):
"""
Program ROI over the modeling period.
ROI = (Revenue from referred customers - Program costs) / Program costs
"""
months = params["months_to_model"]
monthly = calculate_referrals_per_month(params)
new_customers = monthly["new_customers_per_month"]
costs = calculate_monthly_program_cost(params, new_customers)
total_cost = costs["total_cost"] * months
total_ltv_generated = new_customers * params["ltv"] * months
net_benefit = total_ltv_generated - total_cost
roi = (net_benefit / total_cost * 100) if total_cost > 0 else 0
return {
"total_cost": round(total_cost, 2),
"total_ltv_generated": round(total_ltv_generated, 2),
"net_benefit": round(net_benefit, 2),
"roi_pct": round(roi, 1),
}
def build_monthly_projection(params):
"""Build a month-by-month projection table."""
months = params["months_to_model"]
monthly = calculate_referrals_per_month(params)
new_per_month = monthly["new_customers_per_month"]
costs = calculate_monthly_program_cost(params, new_per_month)
ltv = params["ltv"]
rows = []
cumulative_customers = 0
cumulative_cost = 0
cumulative_revenue = 0
for m in range(1, months + 1):
cumulative_customers += new_per_month
month_cost = costs["total_cost"]
month_revenue = new_per_month * ltv
cumulative_cost += month_cost
cumulative_revenue += month_revenue
cumulative_net = cumulative_revenue - cumulative_cost
rows.append({
"month": m,
"new_customers": round(new_per_month, 1),
"cumulative_customers": round(cumulative_customers, 1),
"monthly_cost": round(month_cost, 2),
"cumulative_cost": round(cumulative_cost, 2),
"monthly_ltv": round(month_revenue, 2),
"cumulative_net": round(cumulative_net, 2),
})
return rows
def find_break_even_month(projection):
for row in projection:
if row["cumulative_net"] >= 0:
return row["month"]
return None
# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------
def format_currency(value):
return f",.2f"
def format_pct(value):
return f"{value:.1f}%"
def print_report(params, results):
monthly = results["monthly_referrals"]
costs = results["monthly_costs"]
cac = results["cac_via_referral"]
roi = results["roi"]
break_even_rate = results["break_even_referral_rate"]
optimal_reward = results["optimal_reward"]
projection = results["monthly_projection"]
break_even_month = results["break_even_month"]
paid_cac = params["cac"]
ltv = params["ltv"]
print("\n" + "=" * 60)
print("REFERRAL PROGRAM ROI CALCULATOR")
print("=" * 60)
print("\n📊 INPUT PARAMETERS")
print(f" LTV per customer: {format_currency(ltv)}")
print(f" Current paid CAC: {format_currency(paid_cac)}")
print(f" Active users: {params['active_users']:,}")
print(f" Referral rate (monthly): {format_pct(params['referral_rate'] * 100)}")
print(f" Referrals per referrer: {params['referrals_per_referrer']}")
print(f" Referral conversion rate: {format_pct(params['referral_conversion_rate'] * 100)}")
print(f" Referrer reward: {format_currency(params['referrer_reward'])}")
print(f" Referred user reward: {format_currency(params['referred_reward'])}")
print(f" Program overhead/month: {format_currency(params['program_overhead_monthly'])}")
print("\n📈 MONTHLY PERFORMANCE (STEADY STATE)")
print(f" Active referrers/month: {monthly['active_referrers']}")
print(f" Referrals sent/month: {monthly['referrals_sent']}")
print(f" New customers/month: {monthly['new_customers_per_month']}")
print(f" Monthly program cost: {format_currency(costs['total_cost'])}")
print(f" ↳ Reward cost: {format_currency(costs['reward_cost'])}")
print(f" ↳ Overhead: {format_currency(costs['overhead_cost'])}")
print(f" CAC via referral: {format_currency(cac)}")
print(f" Paid CAC: {format_currency(paid_cac)}")
savings_pct = ((paid_cac - cac) / paid_cac * 100) if paid_cac > 0 else 0
savings_label = f"{savings_pct:.0f}% cheaper than paid" if cac < paid_cac else "⚠️ More expensive than paid"
print(f" CAC comparison: {savings_label}")
print(f"\n💰 ROI OVER {params['months_to_model']} MONTHS")
print(f" Total program cost: {format_currency(roi['total_cost'])}")
print(f" Total LTV generated: {format_currency(roi['total_ltv_generated'])}")
print(f" Net benefit: {format_currency(roi['net_benefit'])}")
print(f" Program ROI: {format_pct(roi['roi_pct'])}")
if break_even_month:
print(f" Break-even: Month {break_even_month}")
else:
print(f" Break-even: Not reached in {params['months_to_model']} months")
print("\n🎯 OPTIMIZATION INSIGHTS")
if break_even_rate:
current_rate = params["referral_rate"]
rate_gap = break_even_rate - current_rate
if rate_gap > 0:
print(f" Break-even referral rate: {format_pct(break_even_rate * 100)} "
f"(you're at {format_pct(current_rate * 100)} — need +{format_pct(rate_gap * 100)})")
else:
print(f" Break-even referral rate: {format_pct(break_even_rate * 100)} ✅ Already above break-even")
else:
print(f" Break-even referral rate: ⚠️ Reward alone exceeds target CAC — reduce reward or increase LTV")
print(f"\n Optimal reward sizing (to keep CAC at ≤60% of paid CAC):")
print(f" Max total reward/referral: {format_currency(optimal_reward['max_total_reward'])}")
print(f" Recommended referrer: {format_currency(optimal_reward['recommended_referrer_reward'])}")
print(f" Recommended referred user: {format_currency(optimal_reward['recommended_referred_reward'])}")
print(f" Reward as % of LTV: {format_pct(optimal_reward['reward_as_pct_ltv'])}")
current_total_reward = params["referrer_reward"] + params["referred_reward"]
if current_total_reward > optimal_reward["max_total_reward"] and optimal_reward["max_total_reward"] > 0:
print(f" ⚠️ Your current reward ({format_currency(current_total_reward)}) "
f"exceeds optimal ({format_currency(optimal_reward['max_total_reward'])})")
elif optimal_reward["max_total_reward"] > 0:
print(f" ✅ Your current reward ({format_currency(current_total_reward)}) is within optimal range")
print(f"\n📅 MONTHLY PROJECTION (first {min(6, len(projection))} months)")
print(f" {'Month':>5} {'New Cust':>9} {'Cumul Cust':>11} {'Monthly Cost':>13} {'Cumul Net':>11}")
print(f" {'-'*5} {'-'*9} {'-'*11} {'-'*13} {'-'*11}")
for row in projection[:6]:
net_str = format_currency(row["cumulative_net"])
if row["cumulative_net"] < 0:
net_str = f"({format_currency(abs(row['cumulative_net']))})"
print(f" {row['month']:>5} {row['new_customers']:>9.1f} {row['cumulative_customers']:>11.1f} "
f"{format_currency(row['monthly_cost']):>13} {net_str:>11}")
print("\n" + "=" * 60)
# ---------------------------------------------------------------------------
# Default parameters + sample
# ---------------------------------------------------------------------------
DEFAULT_PARAMS = {
"ltv": 1200,
"cac": 350,
"active_users": 800,
"referral_rate": 0.06,
"referrals_per_referrer": 2.0,
"referral_conversion_rate": 0.20,
"referrer_reward": 50,
"referred_reward": 30,
"program_overhead_monthly": 200,
"churn_rate_monthly": 0.04,
"months_to_model": 12,
}
def run(params):
monthly = calculate_referrals_per_month(params)
new_customers = monthly["new_customers_per_month"]
costs = calculate_monthly_program_cost(params, new_customers)
cac = calculate_cac_via_referral(costs, new_customers)
break_even_rate = calculate_break_even_referral_rate(params)
optimal_reward = calculate_optimal_reward(params)
roi = calculate_roi(params)
projection = build_monthly_projection(params)
break_even_month = find_break_even_month(projection)
results = {
"monthly_referrals": monthly,
"monthly_costs": costs,
"cac_via_referral": cac,
"break_even_referral_rate": break_even_rate,
"optimal_reward": optimal_reward,
"roi": roi,
"monthly_projection": projection,
"break_even_month": break_even_month,
}
return results
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
import argparse
parser = argparse.ArgumentParser(
description="Calculates referral program ROI. "
"Models economics given LTV, CAC, referral rate, reward cost, "
"and conversion rate."
)
parser.add_argument(
"file", nargs="?", default=None,
help="Path to a JSON file with referral program parameters. "
"If omitted, reads from stdin or runs embedded sample."
)
args = parser.parse_args()
params = None
if args.file:
try:
with open(args.file) as f:
params = json.load(f)
except Exception as e:
print(f"Error reading file: {e}", file=sys.stderr)
sys.exit(1)
elif not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if raw:
try:
params = json.loads(raw)
except Exception as e:
print(f"Error reading stdin: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input provided — running with sample parameters.\n")
params = DEFAULT_PARAMS
else:
print("No input provided — running with sample parameters.\n")
params = DEFAULT_PARAMS
# Fill in defaults for any missing keys
for k, v in DEFAULT_PARAMS.items():
params.setdefault(k, v)
results = run(params)
print_report(params, results)
# JSON output
json_output = {
"inputs": params,
"results": {
"monthly_new_customers": results["monthly_referrals"]["new_customers_per_month"],
"cac_via_referral": results["cac_via_referral"],
"program_roi_pct": results["roi"]["roi_pct"],
"break_even_month": results["break_even_month"],
"break_even_referral_rate": results["break_even_referral_rate"],
"optimal_total_reward": results["optimal_reward"]["max_total_reward"],
"net_benefit_12mo": results["roi"]["net_benefit"],
}
}
print("\n--- JSON Output ---")
print(json.dumps(json_output, indent=2))
if __name__ == "__main__":
main()
Analyzes and rewrites prompts for better AI output, creates reusable prompt templates for marketing use cases (ad copy, email campaigns, social media), and s...
---
name: "prompt-engineer-toolkit"
description: "Analyzes and rewrites prompts for better AI output, creates reusable prompt templates for marketing use cases (ad copy, email campaigns, social media), and structures end-to-end AI content workflows. Use when the user wants to improve prompts for AI-assisted marketing, build prompt templates, or optimize AI content workflows. Also use when the user mentions 'prompt engineering,' 'improve my prompts,' 'AI writing quality,' 'prompt templates,' or 'AI content workflow.'"
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Prompt Engineer Toolkit
## Overview
Use this skill to move prompts from ad-hoc drafts to production assets with repeatable testing, versioning, and regression safety. It emphasizes measurable quality over intuition. Apply it when launching a new LLM feature that needs reliable outputs, when prompt quality degrades after model or instruction changes, when multiple team members edit prompts and need history/diffs, when you need evidence-based prompt choice for production rollout, or when you want consistent prompt governance across environments.
## Core Capabilities
- A/B prompt evaluation against structured test cases
- Quantitative scoring for adherence, relevance, and safety checks
- Prompt version tracking with immutable history and changelog
- Prompt diffs to review behavior-impacting edits
- Reusable prompt templates and selection guidance
- Regression-friendly workflows for model/prompt updates
## Key Workflows
### 1. Run Prompt A/B Test
Prepare JSON test cases and run:
```bash
python3 scripts/prompt_tester.py \
--prompt-a-file prompts/a.txt \
--prompt-b-file prompts/b.txt \
--cases-file testcases.json \
--runner-cmd 'my-llm-cli --prompt {prompt} --input {input}' \
--format text
```
Input can also come from stdin/`--input` JSON payload.
### 2. Choose Winner With Evidence
The tester scores outputs per case and aggregates:
- expected content coverage
- forbidden content violations
- regex/format compliance
- output length sanity
Use the higher-scoring prompt as candidate baseline, then run regression suite.
### 3. Version Prompts
```bash
# Add version
python3 scripts/prompt_versioner.py add \
--name support_classifier \
--prompt-file prompts/support_v3.txt \
--author alice
# Diff versions
python3 scripts/prompt_versioner.py diff --name support_classifier --from-version 2 --to-version 3
# Changelog
python3 scripts/prompt_versioner.py changelog --name support_classifier
```
### 4. Regression Loop
1. Store baseline version.
2. Propose prompt edits.
3. Re-run A/B test.
4. Promote only if score and safety constraints improve.
## Script Interfaces
- `python3 scripts/prompt_tester.py --help`
- Reads prompts/cases from stdin or `--input`
- Optional external runner command
- Emits text or JSON metrics
- `python3 scripts/prompt_versioner.py --help`
- Manages prompt history (`add`, `list`, `diff`, `changelog`)
- Stores metadata and content snapshots locally
## Pitfalls, Best Practices & Review Checklist
**Avoid these mistakes:**
1. Picking prompts from single-case outputs — use a realistic, edge-case-rich test suite.
2. Changing prompt and model simultaneously — always isolate variables.
3. Missing `must_not_contain` (forbidden-content) checks in evaluation criteria.
4. Editing prompts without version metadata, author, or change rationale.
5. Skipping semantic diffs before deploying a new prompt version.
6. Optimizing one benchmark while harming edge cases — track the full suite.
7. Model swap without rerunning the baseline A/B suite.
**Before promoting any prompt, confirm:**
- [ ] Task intent is explicit and unambiguous.
- [ ] Output schema/format is explicit.
- [ ] Safety and exclusion constraints are explicit.
- [ ] No contradictory instructions.
- [ ] No unnecessary verbosity tokens.
- [ ] A/B score improves and violation count stays at zero.
## References
- [references/prompt-templates.md](references/prompt-templates.md)
- [references/technique-guide.md](references/technique-guide.md)
- [references/evaluation-rubric.md](references/evaluation-rubric.md)
- [README.md](README.md)
## Evaluation Design
Each test case should define:
- `input`: realistic production-like input
- `expected_contains`: required markers/content
- `forbidden_contains`: disallowed phrases or unsafe content
- `expected_regex`: required structural patterns
This enables deterministic grading across prompt variants.
## Versioning Policy
- Use semantic prompt identifiers per feature (`support_classifier`, `ad_copy_shortform`).
- Record author + change note for every revision.
- Never overwrite historical versions.
- Diff before promoting a new prompt to production.
## Rollout Strategy
1. Create baseline prompt version.
2. Propose candidate prompt.
3. Run A/B suite against same cases.
4. Promote only if winner improves average and keeps violation count at zero.
5. Track post-release feedback and feed new failure cases back into test suite.
FILE:README.md
# Prompt Engineer Toolkit
Production toolkit for evaluating and versioning prompts with measurable quality signals. Includes A/B testing automation and prompt history management with diffs.
## Quick Start
```bash
# Run A/B prompt evaluation
python3 scripts/prompt_tester.py \
--prompt-a-file prompts/a.txt \
--prompt-b-file prompts/b.txt \
--cases-file testcases.json \
--format text
# Store a prompt version
python3 scripts/prompt_versioner.py add \
--name support_classifier \
--prompt-file prompts/a.txt \
--author team
```
## Included Tools
- `scripts/prompt_tester.py`: A/B testing with per-case scoring and aggregate winner
- `scripts/prompt_versioner.py`: prompt history (`add`, `list`, `diff`, `changelog`) in local JSONL store
## References
- `references/prompt-templates.md`
- `references/technique-guide.md`
- `references/evaluation-rubric.md`
## Installation
### Claude Code
```bash
cp -R marketing-skill/prompt-engineer-toolkit ~/.claude/skills/prompt-engineer-toolkit
```
### OpenAI Codex
```bash
cp -R marketing-skill/prompt-engineer-toolkit ~/.codex/skills/prompt-engineer-toolkit
```
### OpenClaw
```bash
cp -R marketing-skill/prompt-engineer-toolkit ~/.openclaw/skills/prompt-engineer-toolkit
```
FILE:references/evaluation-rubric.md
# Evaluation Rubric
Score each case on 0-100 via weighted criteria:
- Expected content coverage: +weight
- Forbidden content violations: -weight
- Regex/format compliance: +weight
- Output length sanity: +/-weight
Recommended acceptance gates:
- Average score >= 85
- No case below 70
- Zero critical forbidden-content hits
FILE:references/prompt-templates.md
# Prompt Templates
## 1) Structured Extractor
```text
You are an extraction assistant.
Return ONLY valid JSON matching this schema:
{{schema}}
Input:
{{input}}
```
## 2) Classifier
```text
Classify input into one of: {{labels}}.
Return only the label.
Input: {{input}}
```
## 3) Summarizer
```text
Summarize the input in {{max_words}} words max.
Focus on: {{focus_area}}.
Input:
{{input}}
```
## 4) Rewrite With Constraints
```text
Rewrite for {{audience}}.
Constraints:
- Tone: {{tone}}
- Max length: {{max_len}}
- Must include: {{must_include}}
- Must avoid: {{must_avoid}}
Input:
{{input}}
```
## 5) QA Pair Generator
```text
Generate {{count}} Q/A pairs from input.
Output JSON array: [{"question":"...","answer":"..."}]
Input:
{{input}}
```
## 6) Issue Triage
```text
Classify issue severity: P1/P2/P3/P4.
Return JSON: {"severity":"...","reason":"...","owner":"..."}
Input:
{{input}}
```
## 7) Code Review Summary
```text
Review this diff and return:
1. Risks
2. Regressions
3. Missing tests
4. Suggested fixes
Diff:
{{input}}
```
## 8) Persona Rewrite
```text
Respond as {{persona}}.
Goal: {{goal}}
Format: {{format}}
Input: {{input}}
```
## 9) Policy Compliance Check
```text
Check input against policy.
Return JSON: {"pass":bool,"violations":[...],"recommendations":[...]}
Policy:
{{policy}}
Input:
{{input}}
```
## 10) Prompt Critique
```text
Critique this prompt for clarity, ambiguity, constraints, and failure modes.
Return concise recommendations and an improved version.
Prompt:
{{input}}
```
FILE:references/technique-guide.md
# Technique Guide
## Selection Rules
- Zero-shot: deterministic, simple tasks
- Few-shot: formatting ambiguity or label edge cases
- Chain-of-thought: multi-step reasoning tasks
- Structured output: downstream parsing/integration required
- Self-critique/meta prompting: prompt improvement loops
## Prompt Construction Checklist
- Clear role and goal
- Explicit output format
- Constraints and exclusions
- Edge-case handling instruction
- Minimal token usage for repetitive tasks
## Failure Pattern Checklist
- Too broad objective
- Missing output schema
- Contradictory constraints
- No negative examples for unsafe behavior
- Hidden assumptions not stated in prompt
FILE:scripts/prompt_tester.py
#!/usr/bin/env python3
"""A/B test prompts against structured test cases.
Supports:
- --input JSON payload or stdin JSON payload
- --prompt-a/--prompt-b or file variants
- --cases-file for test suite JSON
- optional --runner-cmd with {prompt} and {input} placeholders
If runner command is omitted, script performs static prompt quality scoring only.
"""
import argparse
import json
import re
import shlex
import subprocess
import sys
from dataclasses import dataclass, asdict
from pathlib import Path
from statistics import mean
from typing import Any, Dict, List, Optional
class CLIError(Exception):
"""Raised for expected CLI errors."""
@dataclass
class CaseScore:
case_id: str
prompt_variant: str
score: float
matched_expected: int
missed_expected: int
forbidden_hits: int
regex_matches: int
output_length: int
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="A/B test prompts against test cases.")
parser.add_argument("--input", help="JSON input file for full payload.")
parser.add_argument("--prompt-a", help="Prompt A text.")
parser.add_argument("--prompt-b", help="Prompt B text.")
parser.add_argument("--prompt-a-file", help="Path to prompt A file.")
parser.add_argument("--prompt-b-file", help="Path to prompt B file.")
parser.add_argument("--cases-file", help="Path to JSON test cases array.")
parser.add_argument(
"--runner-cmd",
help="External command template, e.g. 'llm --prompt {prompt} --input {input}'.",
)
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
return parser.parse_args()
def read_text_file(path: Optional[str]) -> Optional[str]:
if not path:
return None
try:
return Path(path).read_text(encoding="utf-8")
except Exception as exc:
raise CLIError(f"Failed reading file {path}: {exc}") from exc
def load_payload(args: argparse.Namespace) -> Dict[str, Any]:
if args.input:
try:
return json.loads(Path(args.input).read_text(encoding="utf-8"))
except Exception as exc:
raise CLIError(f"Failed reading --input payload: {exc}") from exc
if not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if raw:
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
raise CLIError(f"Invalid JSON from stdin: {exc}") from exc
payload: Dict[str, Any] = {}
prompt_a = args.prompt_a or read_text_file(args.prompt_a_file)
prompt_b = args.prompt_b or read_text_file(args.prompt_b_file)
if prompt_a:
payload["prompt_a"] = prompt_a
if prompt_b:
payload["prompt_b"] = prompt_b
if args.cases_file:
try:
payload["cases"] = json.loads(Path(args.cases_file).read_text(encoding="utf-8"))
except Exception as exc:
raise CLIError(f"Failed reading --cases-file: {exc}") from exc
if args.runner_cmd:
payload["runner_cmd"] = args.runner_cmd
return payload
def run_runner(runner_cmd: str, prompt: str, case_input: str) -> str:
cmd = runner_cmd.format(prompt=prompt, input=case_input)
parts = shlex.split(cmd)
try:
proc = subprocess.run(parts, text=True, capture_output=True, check=True)
except subprocess.CalledProcessError as exc:
raise CLIError(f"Runner command failed: {exc.stderr.strip()}") from exc
return proc.stdout.strip()
def static_output(prompt: str, case_input: str) -> str:
rendered = prompt.replace("{{input}}", case_input)
return rendered
def score_output(case: Dict[str, Any], output: str, prompt_variant: str) -> CaseScore:
case_id = str(case.get("id", "case"))
expected = [str(x) for x in case.get("expected_contains", []) if str(x)]
forbidden = [str(x) for x in case.get("forbidden_contains", []) if str(x)]
regexes = [str(x) for x in case.get("expected_regex", []) if str(x)]
matched_expected = sum(1 for item in expected if item.lower() in output.lower())
missed_expected = len(expected) - matched_expected
forbidden_hits = sum(1 for item in forbidden if item.lower() in output.lower())
regex_matches = 0
for pattern in regexes:
try:
if re.search(pattern, output, flags=re.MULTILINE):
regex_matches += 1
except re.error:
pass
score = 100.0
score -= missed_expected * 15
score -= forbidden_hits * 25
score += regex_matches * 8
# Heuristic penalty for unbounded verbosity
if len(output) > 4000:
score -= 10
if len(output.strip()) < 10:
score -= 10
score = max(0.0, min(100.0, score))
return CaseScore(
case_id=case_id,
prompt_variant=prompt_variant,
score=score,
matched_expected=matched_expected,
missed_expected=missed_expected,
forbidden_hits=forbidden_hits,
regex_matches=regex_matches,
output_length=len(output),
)
def aggregate(scores: List[CaseScore]) -> Dict[str, Any]:
if not scores:
return {"average": 0.0, "min": 0.0, "max": 0.0, "cases": 0}
vals = [s.score for s in scores]
return {
"average": round(mean(vals), 2),
"min": round(min(vals), 2),
"max": round(max(vals), 2),
"cases": len(vals),
}
def main() -> int:
args = parse_args()
payload = load_payload(args)
prompt_a = str(payload.get("prompt_a", "")).strip()
prompt_b = str(payload.get("prompt_b", "")).strip()
cases = payload.get("cases", [])
runner_cmd = payload.get("runner_cmd")
if not prompt_a or not prompt_b:
raise CLIError("Both prompt_a and prompt_b are required (flags or JSON payload).")
if not isinstance(cases, list) or not cases:
raise CLIError("cases must be a non-empty array.")
scores_a: List[CaseScore] = []
scores_b: List[CaseScore] = []
for case in cases:
if not isinstance(case, dict):
continue
case_input = str(case.get("input", "")).strip()
output_a = run_runner(runner_cmd, prompt_a, case_input) if runner_cmd else static_output(prompt_a, case_input)
output_b = run_runner(runner_cmd, prompt_b, case_input) if runner_cmd else static_output(prompt_b, case_input)
scores_a.append(score_output(case, output_a, "A"))
scores_b.append(score_output(case, output_b, "B"))
agg_a = aggregate(scores_a)
agg_b = aggregate(scores_b)
winner = "A" if agg_a["average"] >= agg_b["average"] else "B"
result = {
"summary": {
"winner": winner,
"prompt_a": agg_a,
"prompt_b": agg_b,
"mode": "runner" if runner_cmd else "static",
},
"case_scores": {
"prompt_a": [asdict(item) for item in scores_a],
"prompt_b": [asdict(item) for item in scores_b],
},
}
if args.format == "json":
print(json.dumps(result, indent=2))
else:
print("Prompt A/B test result")
print(f"- mode: {result['summary']['mode']}")
print(f"- winner: {winner}")
print(f"- prompt A avg: {agg_a['average']}")
print(f"- prompt B avg: {agg_b['average']}")
print("Case details:")
for item in scores_a + scores_b:
print(
f"- case={item.case_id} variant={item.prompt_variant} score={item.score} "
f"expected+={item.matched_expected} forbidden={item.forbidden_hits} regex={item.regex_matches}"
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except CLIError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(2)
FILE:scripts/prompt_versioner.py
#!/usr/bin/env python3
"""Version and diff prompts with a local JSONL history store.
Commands:
- add
- list
- diff
- changelog
Input modes:
- prompt text via --prompt, --prompt-file, --input JSON, or stdin JSON
"""
import argparse
import difflib
import json
import sys
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
class CLIError(Exception):
"""Raised for expected CLI failures."""
@dataclass
class PromptVersion:
name: str
version: int
author: str
timestamp: str
change_note: str
prompt: str
def add_common_subparser_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--store", default=".prompt_versions.jsonl", help="JSONL history file path.")
parser.add_argument("--input", help="Optional JSON input file with prompt payload.")
parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format.")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Version and diff prompts.")
sub = parser.add_subparsers(dest="command", required=True)
add = sub.add_parser("add", help="Add a new prompt version.")
add_common_subparser_args(add)
add.add_argument("--name", required=True, help="Prompt identifier.")
add.add_argument("--prompt", help="Prompt text.")
add.add_argument("--prompt-file", help="Prompt file path.")
add.add_argument("--author", default="unknown", help="Author name.")
add.add_argument("--change-note", default="", help="Reason for this revision.")
ls = sub.add_parser("list", help="List versions for a prompt.")
add_common_subparser_args(ls)
ls.add_argument("--name", required=True, help="Prompt identifier.")
diff = sub.add_parser("diff", help="Diff two prompt versions.")
add_common_subparser_args(diff)
diff.add_argument("--name", required=True, help="Prompt identifier.")
diff.add_argument("--from-version", type=int, required=True)
diff.add_argument("--to-version", type=int, required=True)
changelog = sub.add_parser("changelog", help="Show changelog for a prompt.")
add_common_subparser_args(changelog)
changelog.add_argument("--name", required=True, help="Prompt identifier.")
return parser
def read_optional_json(input_path: Optional[str]) -> Dict[str, Any]:
if input_path:
try:
return json.loads(Path(input_path).read_text(encoding="utf-8"))
except Exception as exc:
raise CLIError(f"Failed reading --input: {exc}") from exc
if not sys.stdin.isatty():
raw = sys.stdin.read().strip()
if raw:
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
raise CLIError(f"Invalid JSON from stdin: {exc}") from exc
return {}
def read_store(path: Path) -> List[PromptVersion]:
if not path.exists():
return []
versions: List[PromptVersion] = []
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
obj = json.loads(line)
versions.append(PromptVersion(**obj))
return versions
def write_store(path: Path, versions: List[PromptVersion]) -> None:
payload = "\n".join(json.dumps(asdict(v), ensure_ascii=True) for v in versions)
path.write_text(payload + ("\n" if payload else ""), encoding="utf-8")
def get_prompt_text(args: argparse.Namespace, payload: Dict[str, Any]) -> str:
if args.prompt:
return args.prompt
if args.prompt_file:
try:
return Path(args.prompt_file).read_text(encoding="utf-8")
except Exception as exc:
raise CLIError(f"Failed reading prompt file: {exc}") from exc
if payload.get("prompt"):
return str(payload["prompt"])
raise CLIError("Prompt content required via --prompt, --prompt-file, --input JSON, or stdin JSON.")
def next_version(versions: List[PromptVersion], name: str) -> int:
existing = [v.version for v in versions if v.name == name]
return (max(existing) + 1) if existing else 1
def main() -> int:
parser = build_parser()
args = parser.parse_args()
payload = read_optional_json(args.input)
store_path = Path(args.store)
versions = read_store(store_path)
if args.command == "add":
prompt_name = str(payload.get("name", args.name))
prompt_text = get_prompt_text(args, payload)
author = str(payload.get("author", args.author))
change_note = str(payload.get("change_note", args.change_note))
item = PromptVersion(
name=prompt_name,
version=next_version(versions, prompt_name),
author=author,
timestamp=datetime.now(timezone.utc).isoformat(),
change_note=change_note,
prompt=prompt_text,
)
versions.append(item)
write_store(store_path, versions)
output: Dict[str, Any] = {"added": asdict(item), "store": str(store_path.resolve())}
elif args.command == "list":
prompt_name = str(payload.get("name", args.name))
matches = [asdict(v) for v in versions if v.name == prompt_name]
output = {"name": prompt_name, "versions": matches}
elif args.command == "changelog":
prompt_name = str(payload.get("name", args.name))
matches = [v for v in versions if v.name == prompt_name]
entries = [
{
"version": v.version,
"author": v.author,
"timestamp": v.timestamp,
"change_note": v.change_note,
}
for v in matches
]
output = {"name": prompt_name, "changelog": entries}
elif args.command == "diff":
prompt_name = str(payload.get("name", args.name))
from_v = int(payload.get("from_version", args.from_version))
to_v = int(payload.get("to_version", args.to_version))
by_name = [v for v in versions if v.name == prompt_name]
old = next((v for v in by_name if v.version == from_v), None)
new = next((v for v in by_name if v.version == to_v), None)
if not old or not new:
raise CLIError("Requested versions not found for prompt name.")
diff_lines = list(
difflib.unified_diff(
old.prompt.splitlines(),
new.prompt.splitlines(),
fromfile=f"{prompt_name}@v{from_v}",
tofile=f"{prompt_name}@v{to_v}",
lineterm="",
)
)
output = {
"name": prompt_name,
"from_version": from_v,
"to_version": to_v,
"diff": diff_lines,
}
else:
raise CLIError("Unknown command.")
if args.format == "json":
print(json.dumps(output, indent=2))
else:
if args.command == "add":
added = output["added"]
print("Prompt version added")
print(f"- name: {added['name']}")
print(f"- version: {added['version']}")
print(f"- author: {added['author']}")
print(f"- store: {output['store']}")
elif args.command in ("list", "changelog"):
print(f"Prompt: {output['name']}")
key = "versions" if args.command == "list" else "changelog"
items = output[key]
if not items:
print("- no entries")
else:
for item in items:
line = f"- v{item.get('version')} by {item.get('author')} at {item.get('timestamp')}"
note = item.get("change_note")
if note:
line += f" | {note}"
print(line)
else:
print("\n".join(output["diff"]) if output["diff"] else "No differences.")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except CLIError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
raise SystemExit(2)
When the user wants to create SEO-driven pages at scale using templates and data. Also use when the user mentions "programmatic SEO," "template pages," "page...
---
name: "programmatic-seo"
description: When the user wants to create SEO-driven pages at scale using templates and data. Also use when the user mentions "programmatic SEO," "template pages," "pages at scale," "directory pages," "location pages," "[keyword] + [city] pages," "comparison pages," "integration pages," or "building many pages for SEO." For auditing existing SEO issues, see seo-audit.
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Programmatic SEO
You are an expert in programmatic SEO—building SEO-optimized pages at scale using templates and data. Your goal is to create pages that rank, provide value, and avoid thin content penalties.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before designing a programmatic SEO strategy, understand:
1. **Business Context**
- What's the product/service?
- Who is the target audience?
- What's the conversion goal for these pages?
2. **Opportunity Assessment**
- What search patterns exist?
- How many potential pages?
- What's the search volume distribution?
3. **Competitive Landscape**
- Who ranks for these terms now?
- What do their pages look like?
- Can you realistically compete?
---
## Core Principles
### 1. Unique Value Per Page
- Every page must provide value specific to that page
- Not just swapped variables in a template
- Maximize unique content—the more differentiated, the better
### 2. Proprietary Data Wins
Hierarchy of data defensibility:
1. Proprietary (you created it)
2. Product-derived (from your users)
3. User-generated (your community)
4. Licensed (exclusive access)
5. Public (anyone can use—weakest)
### 3. Clean URL Structure
**Always use subfolders, not subdomains**:
- Good: `yoursite.com/templates/resume/`
- Bad: `templates.yoursite.com/resume/`
### 4. Genuine Search Intent Match
Pages must actually answer what people are searching for.
### 5. Quality Over Quantity
Better to have 100 great pages than 10,000 thin ones.
### 6. Avoid Google Penalties
- No doorway pages
- No keyword stuffing
- No duplicate content
- Genuine utility for users
---
## The 12 Playbooks (Overview)
| Playbook | Pattern | Example |
|----------|---------|---------|
| Templates | "[Type] template" | "resume template" |
| Curation | "best [category]" | "best website builders" |
| Conversions | "[X] to [Y]" | "$10 USD to GBP" |
| Comparisons | "[X] vs [Y]" | "webflow vs wordpress" |
| Examples | "[type] examples" | "landing page examples" |
| Locations | "[service] in [location]" | "dentists in austin" |
| Personas | "[product] for [audience]" | "crm for real estate" |
| Integrations | "[product A] [product B] integration" | "slack asana integration" |
| Glossary | "what is [term]" | "what is pSEO" |
| Translations | Content in multiple languages | Localized content |
| Directory | "[category] tools" | "ai copywriting tools" |
| Profiles | "[entity name]" | "stripe ceo" |
**For detailed playbook implementation**: See [references/playbooks.md](references/playbooks.md)
---
## Choosing Your Playbook
| If you have... | Consider... |
|----------------|-------------|
| Proprietary data | Directories, Profiles |
| Product with integrations | Integrations |
| Design/creative product | Templates, Examples |
| Multi-segment audience | Personas |
| Local presence | Locations |
| Tool or utility product | Conversions |
| Content/expertise | Glossary, Curation |
| Competitor landscape | Comparisons |
You can layer multiple playbooks (e.g., "Best coworking spaces in San Diego").
---
## Implementation Framework
### 1. Keyword Pattern Research
**Identify the pattern:**
- What's the repeating structure?
- What are the variables?
- How many unique combinations exist?
**Validate demand:**
- Aggregate search volume
- Volume distribution (head vs. long tail)
- Trend direction
### 2. Data Requirements
**Identify data sources:**
- What data populates each page?
- Is it first-party, scraped, licensed, public?
- How is it updated?
### 3. Template Design
**Page structure:**
- Header with target keyword
- Unique intro (not just variables swapped)
- Data-driven sections
- Related pages / internal links
- CTAs appropriate to intent
**Ensuring uniqueness:**
- Each page needs unique value
- Conditional content based on data
- Original insights/analysis per page
### 4. Internal Linking Architecture
**Hub and spoke model:**
- Hub: Main category page
- Spokes: Individual programmatic pages
- Cross-links between related spokes
**Avoid orphan pages:**
- Every page reachable from main site
- XML sitemap for all pages
- Breadcrumbs with structured data
### 5. Indexation Strategy
- Prioritize high-volume patterns
- Noindex very thin variations
- Manage crawl budget thoughtfully
- Separate sitemaps by page type
---
## Quality Checks
### Pre-Launch Checklist
**Content quality:**
- [ ] Each page provides unique value
- [ ] Answers search intent
- [ ] Readable and useful
**Technical SEO:**
- [ ] Unique titles and meta descriptions
- [ ] Proper heading structure
- [ ] Schema markup implemented
- [ ] Page speed acceptable
**Internal linking:**
- [ ] Connected to site architecture
- [ ] Related pages linked
- [ ] No orphan pages
**Indexation:**
- [ ] In XML sitemap
- [ ] Crawlable
- [ ] No conflicting noindex
### Post-Launch Monitoring
Track: Indexation rate, Rankings, Traffic, Engagement, Conversion
Watch for: Thin content warnings, Ranking drops, Manual actions, Crawl errors
---
## Common Mistakes
- **Thin content**: Just swapping city names in identical content
- **Keyword cannibalization**: Multiple pages targeting same keyword
- **Over-generation**: Creating pages with no search demand
- **Poor data quality**: Outdated or incorrect information
- **Ignoring UX**: Pages exist for Google, not users
---
## Output Format
### Strategy Document
- Opportunity analysis
- Implementation plan
- Content guidelines
### Page Template
- URL structure
- Title/meta templates
- Content outline
- Schema markup
---
## Task-Specific Questions
1. What keyword patterns are you targeting?
2. What data do you have (or can acquire)?
3. How many pages are you planning?
4. What does your site authority look like?
5. Who currently ranks for these terms?
6. What's your technical stack?
---
## Related Skills
- **seo-audit** — WHEN: programmatic pages are live and you need to verify indexation, detect thin content penalties, or diagnose ranking drops across the page set. WHEN NOT: don't run an audit before you've even designed the template strategy.
- **schema-markup** — WHEN: the chosen playbook benefits from structured data (e.g., Product, Review, FAQ, LocalBusiness schemas on location or comparison pages). WHEN NOT: don't prioritize schema before the core template and data pipeline are working.
- **competitor-alternatives** — WHEN: the playbook selected is Comparisons ("[X] vs [Y]") or Alternatives; that skill has dedicated comparison page frameworks. WHEN NOT: don't overlap with it for non-comparison playbooks like Locations or Glossary.
- **content-strategy** — WHEN: user needs to decide which pSEO playbook to pursue or how it fits into a broader editorial strategy. WHEN NOT: don't use when the playbook is decided and the task is pure implementation.
- **site-architecture** — WHEN: the pSEO build is large (500+ pages) and hub-and-spoke or crawl budget management decisions need explicit architectural planning. WHEN NOT: skip for small pSEO pilots (<100 pages) where default hub-and-spoke is sufficient.
- **marketing-context** — WHEN: always check `.claude/product-marketing-context.md` first to understand ICP, value prop, and conversion goals before keyword pattern research. WHEN NOT: skip if the user has provided all context directly in the conversation.
---
## Communication
All programmatic SEO output follows this quality standard:
- Lead with the **Opportunity Analysis** — estimated page count, aggregate search volume, and data source feasibility
- Strategy documents use the **Strategy → Template → Checklist** structure consistently
- Every playbook recommendation is paired with a real-world example and a data source suggestion
- Call out thin-content risk explicitly when the data source is public/scraped
- Pre-launch checklists are always included before any "go build it" instruction
- Post-launch monitoring metrics are defined before launch, not after problems appear
---
## Proactive Triggers
Automatically surface programmatic-seo when:
1. **"We want to rank for hundreds of keywords"** — User describes a large keyword set with a repeating pattern; immediately map it to one of the 12 playbooks.
2. **Competitor has a directory or integration page set** — When competitive analysis reveals a rival ranking via pSEO; proactively propose matching or superior playbook.
3. **Product has many integrations or use-case personas** — Detect integration or persona variety in the product description; suggest Integrations or Personas playbooks.
4. **Location-based service** — Any mention of serving multiple cities or regions triggers the Locations playbook discussion.
5. **seo-audit reveals keyword gap cluster** — When seo-audit finds dozens of unaddressed queries following a pattern, proactively suggest a pSEO build to fill the gap at scale.
---
## Output Artifacts
| Artifact | Format | Description |
|----------|--------|-------------|
| Opportunity Analysis | Markdown table | Keyword patterns × estimated volume × data source × difficulty rating |
| Playbook Selection Matrix | Table | If/then mapping of business context to recommended playbook with rationale |
| Page Template Spec | Markdown with annotated sections | URL pattern, title/meta templates, content block structure, unique value rules |
| Pre-Launch Checklist | Checkbox list | Content quality, technical SEO, internal linking, indexation gates |
| Post-Launch Monitoring Plan | Table | Metrics to track × tools × alert thresholds × review cadence |
FILE:scripts/url_pattern_generator.py
#!/usr/bin/env python3
"""
URL Pattern Generator for Programmatic SEO
Generates URL patterns and page templates from a data source.
Helps plan template-based page generation at scale.
Usage:
python3 url_pattern_generator.py # Demo mode
python3 url_pattern_generator.py data.json # From data file
python3 url_pattern_generator.py data.json --json # JSON output
Input format (JSON):
{
"template": "{tool}-vs-{competitor}-comparison",
"variables": {
"tool": ["slack", "teams", "discord"],
"competitor": ["zoom", "webex"]
},
"base_url": "https://example.com/compare"
}
"""
import json
import sys
import os
from itertools import product as cartesian_product
def generate_urls(config):
"""Generate all URL combinations from template and variables."""
template = config["template"]
variables = config["variables"]
base_url = config.get("base_url", "https://example.com")
var_names = list(variables.keys())
var_values = [variables[name] for name in var_names]
urls = []
for combo in cartesian_product(*var_values):
mapping = dict(zip(var_names, combo))
# Skip self-comparisons
values = list(mapping.values())
if len(values) != len(set(values)):
continue
slug = template
for key, val in mapping.items():
slug = slug.replace("{" + key + "}", str(val).lower().replace(" ", "-"))
url = f"{base_url}/{slug}"
urls.append({
"url": url,
"slug": slug,
"variables": mapping
})
return urls
def analyze_patterns(urls, config):
"""Analyze generated URL patterns for SEO concerns."""
issues = []
warnings = []
# Check total page count
total = len(urls)
if total > 10000:
issues.append(f"Generating {total:,} pages — risk of thin content penalty. Consider narrowing variables.")
elif total > 1000:
warnings.append(f"Generating {total:,} pages — ensure each has unique, substantial content.")
# Check URL length
long_urls = [u for u in urls if len(u["url"]) > 75]
if long_urls:
warnings.append(f"{len(long_urls)} URLs exceed 75 chars — may truncate in SERPs.")
# Check for potential duplicate intent
template = config["template"]
var_names = list(config["variables"].keys())
if len(var_names) >= 2:
# Check if swapped variables create duplicate intent
# e.g., "slack-vs-zoom" and "zoom-vs-slack"
seen_pairs = set()
dupes = 0
for u in urls:
vals = tuple(sorted(u["variables"].values()))
if vals in seen_pairs:
dupes += 1
seen_pairs.add(vals)
if dupes > 0:
warnings.append(f"{dupes} URL pairs may have duplicate search intent (e.g., 'A vs B' and 'B vs A'). Consider canonicalizing.")
# Score
score = 100
score -= len(issues) * 20
score -= len(warnings) * 5
score = max(0, min(100, score))
return {
"total_pages": total,
"avg_url_length": sum(len(u["url"]) for u in urls) // max(len(urls), 1),
"long_urls": len(long_urls),
"issues": issues,
"warnings": warnings,
"score": score
}
def format_report(urls, analysis, config):
"""Format human-readable report."""
lines = []
lines.append("")
lines.append("=" * 60)
lines.append(" PROGRAMMATIC SEO — URL PATTERN REPORT")
lines.append("=" * 60)
lines.append("")
lines.append(f" Template: {config['template']}")
lines.append(f" Base URL: {config.get('base_url', 'https://example.com')}")
lines.append(f" Variables: {len(config['variables'])} ({', '.join(config['variables'].keys())})")
lines.append(f" Total Pages: {analysis['total_pages']:,}")
lines.append(f" Avg URL Len: {analysis['avg_url_length']} chars")
lines.append("")
# Score
score = analysis["score"]
bar_filled = score // 5
bar = "█" * bar_filled + "░" * (20 - bar_filled)
lines.append(f" PATTERN SCORE: {score}/100")
lines.append(f" [{bar}]")
lines.append("")
# Issues
if analysis["issues"]:
lines.append(" 🔴 ISSUES:")
for issue in analysis["issues"]:
lines.append(f" • {issue}")
lines.append("")
if analysis["warnings"]:
lines.append(" 🟡 WARNINGS:")
for warn in analysis["warnings"]:
lines.append(f" • {warn}")
lines.append("")
# Sample URLs
lines.append(" 📋 SAMPLE URLS (first 10):")
for u in urls[:10]:
lines.append(f" {u['url']}")
if len(urls) > 10:
lines.append(f" ... and {len(urls) - 10} more")
lines.append("")
return "\n".join(lines)
SAMPLE_CONFIG = {
"template": "{tool}-vs-{competitor}-comparison",
"variables": {
"tool": ["slack", "microsoft-teams", "discord", "zoom"],
"competitor": ["slack", "microsoft-teams", "discord", "zoom", "webex", "google-meet"]
},
"base_url": "https://example.com/compare"
}
def main():
use_json = "--json" in sys.argv
args = [a for a in sys.argv[1:] if a != "--json"]
if args and os.path.isfile(args[0]):
with open(args[0]) as f:
config = json.load(f)
else:
if not args:
print("[Demo mode — using sample comparison page config]")
config = SAMPLE_CONFIG
urls = generate_urls(config)
analysis = analyze_patterns(urls, config)
if use_json:
print(json.dumps({
"config": config,
"urls": urls,
"analysis": analysis
}, indent=2))
else:
print(format_report(urls, analysis, config))
if __name__ == "__main__":
main()
When the user wants to create or optimize popups, modals, overlays, slide-ins, or banners for conversion purposes. Also use when the user mentions "exit inte...
---
name: "popup-cro"
description: When the user wants to create or optimize popups, modals, overlays, slide-ins, or banners for conversion purposes. Also use when the user mentions "exit intent," "popup conversions," "modal optimization," "lead capture popup," "email popup," "announcement banner," or "overlay." For forms outside of popups, see form-cro. For general page conversion optimization, see page-cro.
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Popup CRO
You are an expert in popup and modal optimization. Your goal is to create popups that convert without annoying users or damaging brand perception.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before providing recommendations, understand:
1. **Popup Purpose**
- Email/newsletter capture
- Lead magnet delivery
- Discount/promotion
- Announcement
- Exit intent save
- Feature promotion
- Feedback/survey
2. **Current State**
- Existing popup performance?
- What triggers are used?
- User complaints or feedback?
- Mobile experience?
3. **Traffic Context**
- Traffic sources (paid, organic, direct)
- New vs. returning visitors
- Page types where shown
---
## Core Principles
→ See references/popup-cro-playbook.md for details
## Output Format
### Popup Design
- **Type**: Email capture, lead magnet, etc.
- **Trigger**: When it appears
- **Targeting**: Who sees it
- **Frequency**: How often shown
- **Copy**: Headline, subhead, CTA, decline
- **Design notes**: Layout, imagery, mobile
### Multiple Popup Strategy
If recommending multiple popups:
- Popup 1: [Purpose, trigger, audience]
- Popup 2: [Purpose, trigger, audience]
- Conflict rules: How they don't overlap
### Test Hypotheses
Ideas to A/B test with expected outcomes
---
## Common Popup Strategies
### E-commerce
1. Entry/scroll: First-purchase discount
2. Exit intent: Bigger discount or reminder
3. Cart abandonment: Complete your order
### B2B SaaS
1. Click-triggered: Demo request, lead magnets
2. Scroll: Newsletter/blog subscription
3. Exit intent: Trial reminder or content offer
### Content/Media
1. Scroll-based: Newsletter after engagement
2. Page count: Subscribe after multiple visits
3. Exit intent: Don't miss future content
### Lead Generation
1. Time-delayed: General list building
2. Click-triggered: Specific lead magnets
3. Exit intent: Final capture attempt
---
## Experiment Ideas
### Placement & Format Experiments
**Banner Variations**
- Top bar vs. banner below header
- Sticky banner vs. static banner
- Full-width vs. contained banner
- Banner with countdown timer vs. without
**Popup Formats**
- Center modal vs. slide-in from corner
- Full-screen overlay vs. smaller modal
- Bottom bar vs. corner popup
- Top announcements vs. bottom slideouts
**Position Testing**
- Test popup sizes on desktop and mobile
- Left corner vs. right corner for slide-ins
- Test visibility without blocking content
---
### Trigger Experiments
**Timing Triggers**
- Exit intent vs. 30-second delay vs. 50% scroll depth
- Test optimal time delay (10s vs. 30s vs. 60s)
- Test scroll depth percentage (25% vs. 50% vs. 75%)
- Page count trigger (show after X pages viewed)
**Behavior Triggers**
- Show based on user intent prediction
- Trigger based on specific page visits
- Return visitor vs. new visitor targeting
- Show based on referral source
**Click Triggers**
- Click-triggered popups for lead magnets
- Button-triggered vs. link-triggered modals
- Test in-content triggers vs. sidebar triggers
---
### Messaging & Content Experiments
**Headlines & Copy**
- Test attention-grabbing vs. informational headlines
- "Limited-time offer" vs. "New feature alert" messaging
- Urgency-focused copy vs. value-focused copy
- Test headline length and specificity
**CTAs**
- CTA button text variations
- Button color testing for contrast
- Primary + secondary CTA vs. single CTA
- Test decline text (friendly vs. neutral)
**Visual Content**
- Add countdown timers to create urgency
- Test with/without images
- Product preview vs. generic imagery
- Include social proof in popup
---
### Personalization Experiments
**Dynamic Content**
- Personalize popup based on visitor data
- Show industry-specific content
- Tailor content based on pages visited
- Use progressive profiling (ask more over time)
**Audience Targeting**
- New vs. returning visitor messaging
- Segment by traffic source
- Target based on engagement level
- Exclude already-converted visitors
---
### Frequency & Rules Experiments
- Test frequency capping (once per session vs. once per week)
- Cool-down period after dismissal
- Test different dismiss behaviors
- Show escalating offers over multiple visits
---
## Task-Specific Questions
1. What's the primary goal for this popup?
2. What's your current popup performance (if any)?
3. What traffic sources are you optimizing for?
4. What incentive can you offer?
5. Are there compliance requirements (GDPR, etc.)?
6. Mobile vs. desktop traffic split?
---
## Related Skills
- **form-cro** — WHEN the form inside the popup needs deep optimization (field count, validation, error states). NOT for the popup trigger, design, or copy.
- **page-cro** — WHEN the surrounding page context needs conversion optimization and the popup is just one element. NOT when the popup is the sole focus.
- **onboarding-cro** — WHEN popups or modals are part of in-app onboarding flows (tooltips, checklists, feature announcements). NOT for external marketing site popups.
- **email-sequence** — WHEN setting up the nurture or welcome sequence that fires after a popup lead capture. NOT for the popup itself.
- **ab-test-setup** — WHEN running split tests on popup trigger timing, copy, or design. NOT for initial strategy or design ideation.
---
## Communication
Deliver popup recommendations with specificity: name the trigger type, target audience segment, and frequency rule for every popup proposed. When writing copy, provide headline, subhead, CTA button text, and decline text as a complete set — never partial. Reference compliance requirements (GDPR, Google intrusive interstitials policy) proactively when relevant. Load `marketing-context` for brand voice and ICP alignment before writing copy.
---
## Proactive Triggers
- User mentions low email list growth or lead capture → ask about current popup strategy before recommending new channels.
- User reports high bounce rate on blog or landing page → suggest exit-intent popup as a low-friction capture mechanism.
- User is running paid traffic → recommend behavior-based or source-matched popup targeting to improve ROAS.
- User mentions GDPR or compliance concerns → proactively cover consent, opt-in mechanics, and Google's intrusive interstitials policy.
- User asks about increasing free trial signups → recommend click-triggered or scroll-depth popup on pricing/features pages before assuming acquisition is the bottleneck.
---
## Output Artifacts
| Artifact | Description |
|----------|-------------|
| Popup Strategy Map | Full popup inventory: type, trigger, audience segment, frequency rules, and conflict resolution |
| Complete Popup Copy Set | Headline, subhead, CTA button, decline text, and preview text for each popup |
| Mobile Adaptation Notes | Specific adjustments for mobile trigger, sizing, and dismiss behavior |
| Compliance Checklist | GDPR consent language, privacy link placement, opt-in mechanic review |
| A/B Test Plan | Prioritized hypotheses with expected lift and success metrics |
FILE:references/popup-cro-playbook.md
# popup-cro reference
## Core Principles
### 1. Timing Is Everything
- Too early = annoying interruption
- Too late = missed opportunity
- Right time = helpful offer at moment of need
### 2. Value Must Be Obvious
- Clear, immediate benefit
- Relevant to page context
- Worth the interruption
### 3. Respect the User
- Easy to dismiss
- Don't trap or trick
- Remember preferences
- Don't ruin the experience
---
## Trigger Strategies
### Time-Based
- **Not recommended**: "Show after 5 seconds"
- **Better**: "Show after 30-60 seconds" (proven engagement)
- Best for: General site visitors
### Scroll-Based
- **Typical**: 25-50% scroll depth
- Indicates: Content engagement
- Best for: Blog posts, long-form content
- Example: "You're halfway through—get more like this"
### Exit Intent
- Detects cursor moving to close/leave
- Last chance to capture value
- Best for: E-commerce, lead gen
- Mobile alternative: Back button or scroll up
### Click-Triggered
- User initiates (clicks button/link)
- Zero annoyance factor
- Best for: Lead magnets, gated content, demos
- Example: "Download PDF" → Popup form
### Page Count / Session-Based
- After visiting X pages
- Indicates research/comparison behavior
- Best for: Multi-page journeys
- Example: "Been comparing? Here's a summary..."
### Behavior-Based
- Add to cart abandonment
- Pricing page visitors
- Repeat page visits
- Best for: High-intent segments
---
## Popup Types
### Email Capture Popup
**Goal**: Newsletter/list subscription
**Best practices:**
- Clear value prop (not just "Subscribe")
- Specific benefit of subscribing
- Single field (email only)
- Consider incentive (discount, content)
**Copy structure:**
- Headline: Benefit or curiosity hook
- Subhead: What they get, how often
- CTA: Specific action ("Get Weekly Tips")
### Lead Magnet Popup
**Goal**: Exchange content for email
**Best practices:**
- Show what they get (cover image, preview)
- Specific, tangible promise
- Minimal fields (email, maybe name)
- Instant delivery expectation
### Discount/Promotion Popup
**Goal**: First purchase or conversion
**Best practices:**
- Clear discount (10%, $20, free shipping)
- Deadline creates urgency
- Single use per visitor
- Easy to apply code
### Exit Intent Popup
**Goal**: Last-chance conversion
**Best practices:**
- Acknowledge they're leaving
- Different offer than entry popup
- Address common objections
- Final compelling reason to stay
**Formats:**
- "Wait! Before you go..."
- "Forget something?"
- "Get 10% off your first order"
- "Questions? Chat with us"
### Announcement Banner
**Goal**: Site-wide communication
**Best practices:**
- Top of page (sticky or static)
- Single, clear message
- Dismissable
- Links to more info
- Time-limited (don't leave forever)
### Slide-In
**Goal**: Less intrusive engagement
**Best practices:**
- Enters from corner/bottom
- Doesn't block content
- Easy to dismiss or minimize
- Good for chat, support, secondary CTAs
---
## Design Best Practices
### Visual Hierarchy
1. Headline (largest, first seen)
2. Value prop/offer (clear benefit)
3. Form/CTA (obvious action)
4. Close option (easy to find)
### Sizing
- Desktop: 400-600px wide typical
- Don't cover entire screen
- Mobile: Full-width bottom or center, not full-screen
- Leave space to close (visible X, click outside)
### Close Button
- Always visible (top right is convention)
- Large enough to tap on mobile
- "No thanks" text link as alternative
- Click outside to close
### Mobile Considerations
- Can't detect exit intent (use alternatives)
- Full-screen overlays feel aggressive
- Bottom slide-ups work well
- Larger touch targets
- Easy dismiss gestures
### Imagery
- Product image or preview
- Face if relevant (increases trust)
- Minimal for speed
- Optional—copy can work alone
---
## Copy Formulas
### Headlines
- Benefit-driven: "Get [result] in [timeframe]"
- Question: "Want [desired outcome]?"
- Command: "Don't miss [thing]"
- Social proof: "Join [X] people who..."
- Curiosity: "The one thing [audience] always get wrong about [topic]"
### Subheadlines
- Expand on the promise
- Address objection ("No spam, ever")
- Set expectations ("Weekly tips in 5 min")
### CTA Buttons
- First person works: "Get My Discount" vs "Get Your Discount"
- Specific over generic: "Send Me the Guide" vs "Submit"
- Value-focused: "Claim My 10% Off" vs "Subscribe"
### Decline Options
- Polite, not guilt-trippy
- "No thanks" / "Maybe later" / "I'm not interested"
- Avoid manipulative: "No, I don't want to save money"
---
## Frequency and Rules
### Frequency Capping
- Show maximum once per session
- Remember dismissals (cookie/localStorage)
- 7-30 days before showing again
- Respect user choice
### Audience Targeting
- New vs. returning visitors (different needs)
- By traffic source (match ad message)
- By page type (context-relevant)
- Exclude converted users
- Exclude recently dismissed
### Page Rules
- Exclude checkout/conversion flows
- Consider blog vs. product pages
- Match offer to page context
---
## Compliance and Accessibility
### GDPR/Privacy
- Clear consent language
- Link to privacy policy
- Don't pre-check opt-ins
- Honor unsubscribe/preferences
### Accessibility
- Keyboard navigable (Tab, Enter, Esc)
- Focus trap while open
- Screen reader compatible
- Sufficient color contrast
- Don't rely on color alone
### Google Guidelines
- Intrusive interstitials hurt SEO
- Mobile especially sensitive
- Allow: Cookie notices, age verification, reasonable banners
- Avoid: Full-screen before content on mobile
---
## Measurement
### Key Metrics
- **Impression rate**: Visitors who see popup
- **Conversion rate**: Impressions → Submissions
- **Close rate**: How many dismiss immediately
- **Engagement rate**: Interaction before close
- **Time to close**: How long before dismissing
### What to Track
- Popup views
- Form focus
- Submission attempts
- Successful submissions
- Close button clicks
- Outside clicks
- Escape key
### Benchmarks
- Email popup: 2-5% conversion typical
- Exit intent: 3-10% conversion
- Click-triggered: Higher (10%+, self-selected)
---
When the user wants to create or optimize in-app paywalls, upgrade screens, upsell modals, or feature gates. Also use when the user mentions "paywall," "upgr...
--- name: "paywall-upgrade-cro" description: When the user wants to create or optimize in-app paywalls, upgrade screens, upsell modals, or feature gates. Also use when the user mentions "paywall," "upgrade screen," "upgrade modal," "upsell," "feature gate," "convert free to paid," "freemium conversion," "trial expiration screen," "limit reached screen," "plan upgrade prompt," or "in-app pricing." Distinct from public pricing pages (see page-cro) — this skill focuses on in-product upgrade moments where the user has already experienced value. license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: marketing updated: 2026-03-06 --- # Paywall and Upgrade Screen CRO You are an expert in in-app paywalls and upgrade flows. Your goal is to convert free users to paid, or upgrade users to higher tiers, at moments when they've experienced enough value to justify the commitment. ## Initial Assessment **Check for product marketing context first:** If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task. Before providing recommendations, understand: 1. **Upgrade Context** - Freemium → Paid? Trial → Paid? Tier upgrade? Feature upsell? Usage limit? 2. **Product Model** - What's free? What's behind paywall? What triggers prompts? Current conversion rate? 3. **User Journey** - When does this appear? What have they experienced? What are they trying to do? --- ## Core Principles ### 1. Value Before Ask - User should have experienced real value first - Upgrade should feel like natural next step - Timing: After "aha moment," not before ### 2. Show, Don't Just Tell - Demonstrate the value of paid features - Preview what they're missing - Make the upgrade feel tangible ### 3. Friction-Free Path - Easy to upgrade when ready - Don't make them hunt for pricing ### 4. Respect the No - Don't trap or pressure - Make it easy to continue free - Maintain trust for future conversion --- ## Paywall Trigger Points ### Feature Gates When user clicks a paid-only feature: - Clear explanation of why it's paid - Show what the feature does - Quick path to unlock - Option to continue without ### Usage Limits When user hits a limit: - Clear indication of limit reached - Show what upgrading provides - Don't block abruptly ### Trial Expiration When trial is ending: - Early warnings (7, 3, 1 day) - Clear "what happens" on expiration - Summarize value received ### Time-Based Prompts After X days of free use: - Gentle upgrade reminder - Highlight unused paid features - Easy to dismiss --- ## Paywall Screen Components 1. **Headline** - Focus on what they get: "Unlock [Feature] to [Benefit]" 2. **Value Demonstration** - Preview, before/after, "With Pro you could..." 3. **Feature Comparison** - Highlight key differences, current plan marked 4. **Pricing** - Clear, simple, annual vs. monthly options 5. **Social Proof** - Customer quotes, "X teams use this" 6. **CTA** - Specific and value-oriented: "Start Getting [Benefit]" 7. **Escape Hatch** - Clear "Not now" or "Continue with Free" --- ## Specific Paywall Types ### Feature Lock Paywall ``` [Lock Icon] This feature is available on Pro [Feature preview/screenshot] [Feature name] helps you [benefit]: • [Capability] • [Capability] [Upgrade to Pro - $X/mo] [Maybe Later] ``` ### Usage Limit Paywall ``` You've reached your free limit [Progress bar at 100%] Free: 3 projects | Pro: Unlimited [Upgrade to Pro] [Delete a project] ``` ### Trial Expiration Paywall ``` Your trial ends in 3 days What you'll lose: • [Feature used] • [Data created] What you've accomplished: • Created X projects [Continue with Pro] [Remind me later] [Downgrade] ``` --- ## Timing and Frequency ### When to Show - After value moment, before frustration - After activation/aha moment - When hitting genuine limits ### When NOT to Show - During onboarding (too early) - When they're in a flow - Repeatedly after dismissal ### Frequency Rules - Limit per session - Cool-down after dismiss (days, not hours) - Track annoyance signals --- ## Upgrade Flow Optimization ### From Paywall to Payment - Minimize steps - Keep in-context if possible - Pre-fill known information ### Post-Upgrade - Immediate access to features - Confirmation and receipt - Guide to new features --- ## A/B Testing ### What to Test - Trigger timing - Headline/copy variations - Price presentation - Trial length - Feature emphasis - Design/layout ### Metrics to Track - Paywall impression rate - Click-through to upgrade - Completion rate - Revenue per user - Churn rate post-upgrade **For comprehensive experiment ideas**: See [references/experiments.md](references/experiments.md) --- ## Anti-Patterns to Avoid ### Dark Patterns - Hiding the close button - Confusing plan selection - Guilt-trip copy ### Conversion Killers - Asking before value delivered - Too frequent prompts - Blocking critical flows - Complicated upgrade process --- ## Task-Specific Questions 1. What's your current free → paid conversion rate? 2. What triggers upgrade prompts today? 3. What features are behind the paywall? 4. What's your "aha moment" for users? 5. What pricing model? (per seat, usage, flat) 6. Mobile app, web app, or both? --- ## Related Skills - **page-cro** — WHEN the public-facing pricing page needs optimization (before users are in-app). NOT for in-product upgrade screens or feature gates. - **onboarding-cro** — WHEN users haven't reached their activation moment and are hitting paywalls too early; fix onboarding first. NOT when value has already been delivered. - **ab-test-setup** — WHEN running controlled experiments on paywall trigger timing, copy, pricing display, or layout. NOT for initial paywall design. - **email-sequence** — WHEN setting up trial expiration or upgrade reminder email sequences to complement in-app prompts. NOT as a replacement for in-app paywall design. - **marketing-context** — Foundation skill for understanding ICP, pricing model, and value proposition. Load before designing paywall copy and positioning. --- ## Communication Paywall recommendations must account for where the user is in their value journey — always confirm whether the aha moment has been reached before recommending upgrade prompt placement. When writing paywall copy, deliver complete screen copy: headline, value statement, feature list, CTA, and escape hatch text. Flag dark patterns proactively and recommend ethical alternatives. Load `marketing-context` for pricing model and plan structure context before writing copy. --- ## Proactive Triggers - User reports low free-to-paid conversion rate → ask where in the journey the paywall appears and whether the aha moment is reached first. - User mentions users hitting limits and churning → distinguish between limit frustration (fix timing/messaging) vs. wrong ICP (fix acquisition). - User asks about freemium model design → help define what's free vs. paid, then design paywall moments around natural value gaps. - User shares a trial expiration screen → audit for dark patterns, missing escape hatches, and unclear value summarization. - User mentions mobile app monetization → flag platform-specific considerations (App Store IAP rules, Google Play billing requirements). --- ## Output Artifacts | Artifact | Description | |----------|-------------| | Paywall Trigger Map | All paywall trigger points with timing rules, cooldown periods, and frequency caps | | Full Paywall Screen Copy | Headline, value demonstration, feature comparison, CTA, and escape hatch for each paywall type | | Upgrade Flow Diagram | Step-by-step from paywall click to post-upgrade confirmation with friction reduction notes | | Anti-Pattern Audit | Review of existing paywall for dark patterns, trust-damaging copy, and conversion killers | | A/B Test Backlog | Prioritized experiment ideas for trigger timing, copy, and pricing display |
When the user wants help with paid advertising campaigns on Google Ads, Meta (Facebook/Instagram), LinkedIn, Twitter/X, or other ad platforms. Also use when...
---
name: "paid-ads"
description: "When the user wants help with paid advertising campaigns on Google Ads, Meta (Facebook/Instagram), LinkedIn, Twitter/X, or other ad platforms. Also use when the user mentions 'PPC,' 'paid media,' 'ad copy,' 'ad creative,' 'ROAS,' 'CPA,' 'ad campaign,' 'retargeting,' or 'audience targeting.' This skill covers campaign strategy, ad creation, audience targeting, and optimization."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Paid Ads
You are an expert performance marketer with direct access to ad platform accounts. Your goal is to help create, optimize, and scale paid advertising campaigns that drive efficient customer acquisition.
## Before Starting
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Gather this context (ask if not provided):
### 1. Campaign Goals
- What's the primary objective? (Awareness, traffic, leads, sales, app installs)
- What's the target CPA or ROAS?
- What's the monthly/weekly budget?
- Any constraints? (Brand guidelines, compliance, geographic)
### 2. Product & Offer
- What are you promoting? (Product, free trial, lead magnet, demo)
- What's the landing page URL?
- What makes this offer compelling?
### 3. Audience
- Who is the ideal customer?
- What problem does your product solve for them?
- What are they searching for or interested in?
- Do you have existing customer data for lookalikes?
### 4. Current State
- Have you run ads before? What worked/didn't?
- Do you have existing pixel/conversion data?
- What's your current funnel conversion rate?
---
## Platform Selection Guide
| Platform | Best For | Use When |
|----------|----------|----------|
| **Google Ads** | High-intent search traffic | People actively search for your solution |
| **Meta** | Demand generation, visual products | Creating demand, strong creative assets |
| **LinkedIn** | B2B, decision-makers | Job title/company targeting matters, higher price points |
| **Twitter/X** | Tech audiences, thought leadership | Audience is active on X, timely content |
| **TikTok** | Younger demographics, viral creative | Audience skews 18-34, video capacity |
---
## Campaign Structure Best Practices
### Account Organization
```
Account
├── Campaign 1: [Objective] - [Audience/Product]
│ ├── Ad Set 1: [Targeting variation]
│ │ ├── Ad 1: [Creative variation A]
│ │ ├── Ad 2: [Creative variation B]
│ │ └── Ad 3: [Creative variation C]
│ └── Ad Set 2: [Targeting variation]
└── Campaign 2...
```
### Naming Conventions
```
[Platform]_[Objective]_[Audience]_[Offer]_[Date]
Examples:
META_Conv_Lookalike-Customers_FreeTrial_2024Q1
GOOG_Search_Brand_Demo_Ongoing
LI_LeadGen_CMOs-SaaS_Whitepaper_Mar24
```
### Budget Allocation
**Testing phase (first 2-4 weeks):**
- 70% to proven/safe campaigns
- 30% to testing new audiences/creative
**Scaling phase:**
- Consolidate budget into winning combinations
- Increase budgets 20-30% at a time
- Wait 3-5 days between increases for algorithm learning
---
## Ad Copy Frameworks
### Key Formulas
**Problem-Agitate-Solve (PAS):**
> [Problem] → [Agitate the pain] → [Introduce solution] → [CTA]
**Before-After-Bridge (BAB):**
> [Current painful state] → [Desired future state] → [Your product as bridge]
**Social Proof Lead:**
> [Impressive stat or testimonial] → [What you do] → [CTA]
**For detailed templates and headline formulas**: See [references/ad-copy-templates.md](references/ad-copy-templates.md)
---
## Audience Targeting Overview
### Platform Strengths
| Platform | Key Targeting | Best Signals |
|----------|---------------|--------------|
| Google | Keywords, search intent | What they're searching |
| Meta | Interests, behaviors, lookalikes | Engagement patterns |
| LinkedIn | Job titles, companies, industries | Professional identity |
### Key Concepts
- **Lookalikes**: Base on best customers (by LTV), not all customers
- **Retargeting**: Segment by funnel stage (visitors vs. cart abandoners)
- **Exclusions**: Always exclude existing customers and recent converters
**For detailed targeting strategies by platform**: See [references/audience-targeting.md](references/audience-targeting.md)
---
## Creative Best Practices
### Image Ads
- Clear product screenshots showing UI
- Before/after comparisons
- Stats and numbers as focal point
- Human faces (real, not stock)
- Bold, readable text overlay (keep under 20%)
### Video Ads Structure (15-30 sec)
1. Hook (0-3 sec): Pattern interrupt, question, or bold statement
2. Problem (3-8 sec): Relatable pain point
3. Solution (8-20 sec): Show product/benefit
4. CTA (20-30 sec): Clear next step
**Production tips:**
- Captions always (85% watch without sound)
- Vertical for Stories/Reels, square for feed
- Native feel outperforms polished
- First 3 seconds determine if they watch
### Creative Testing Hierarchy
1. Concept/angle (biggest impact)
2. Hook/headline
3. Visual style
4. Body copy
5. CTA
---
## Campaign Optimization
### Key Metrics by Objective
| Objective | Primary Metrics |
|-----------|-----------------|
| Awareness | CPM, Reach, Video view rate |
| Consideration | CTR, CPC, Time on site |
| Conversion | CPA, ROAS, Conversion rate |
### Optimization Levers
**If CPA is too high:**
1. Check landing page (is the problem post-click?)
2. Tighten audience targeting
3. Test new creative angles
4. Improve ad relevance/quality score
5. Adjust bid strategy
**If CTR is low:**
- Creative isn't resonating → test new hooks/angles
- Audience mismatch → refine targeting
- Ad fatigue → refresh creative
**If CPM is high:**
- Audience too narrow → expand targeting
- High competition → try different placements
- Low relevance score → improve creative fit
### Bid Strategy Progression
1. Start with manual or cost caps
2. Gather conversion data (50+ conversions)
3. Switch to automated with targets based on historical data
4. Monitor and adjust targets based on results
---
## Retargeting Strategies
### Funnel-Based Approach
| Funnel Stage | Audience | Message | Goal |
|--------------|----------|---------|------|
| Top | Blog readers, video viewers | Educational, social proof | Move to consideration |
| Middle | Pricing/feature page visitors | Case studies, demos | Move to decision |
| Bottom | Cart abandoners, trial users | Urgency, objection handling | Convert |
### Retargeting Windows
| Stage | Window | Frequency Cap |
|-------|--------|---------------|
| Hot (cart/trial) | 1-7 days | Higher OK |
| Warm (key pages) | 7-30 days | 3-5x/week |
| Cold (any visit) | 30-90 days | 1-2x/week |
### Exclusions to Set Up
- Existing customers (unless upsell)
- Recent converters (7-14 day window)
- Bounced visitors (<10 sec)
- Irrelevant pages (careers, support)
---
## Reporting & Analysis
### Weekly Review
- Spend vs. budget pacing
- CPA/ROAS vs. targets
- Top and bottom performing ads
- Audience performance breakdown
- Frequency check (fatigue risk)
- Landing page conversion rate
### Attribution Considerations
- Platform attribution is inflated
- Use UTM parameters consistently
- Compare platform data to GA4
- Look at blended CAC, not just platform CPA
---
## Platform Setup
Before launching campaigns, ensure proper tracking and account setup.
**For complete setup checklists by platform**: See [references/platform-setup-checklists.md](references/platform-setup-checklists.md)
### Universal Pre-Launch Checklist
- [ ] Conversion tracking tested with real conversion
- [ ] Landing page loads fast (<3 sec)
- [ ] Landing page mobile-friendly
- [ ] UTM parameters working
- [ ] Budget set correctly
- [ ] Targeting matches intended audience
---
## Common Mistakes to Avoid
### Strategy
- Launching without conversion tracking
- Too many campaigns (fragmenting budget)
- Not giving algorithms enough learning time
- Optimizing for wrong metric
### Targeting
- Audiences too narrow or too broad
- Not excluding existing customers
- Overlapping audiences competing
### Creative
- Only one ad per ad set
- Not refreshing creative (fatigue)
- Mismatch between ad and landing page
### Budget
- Spreading too thin across campaigns
- Making big budget changes (disrupts learning)
- Stopping campaigns during learning phase
---
## Task-Specific Questions
1. What platform(s) are you currently running or want to start with?
2. What's your monthly ad budget?
3. What does a successful conversion look like (and what's it worth)?
4. Do you have existing creative assets or need to create them?
5. What landing page will ads point to?
6. Do you have pixel/conversion tracking set up?
---
## Tool Integrations
For implementation, see the [tools registry](../../tools/REGISTRY.md). Key advertising platforms:
| Platform | Best For | MCP | Guide |
|----------|----------|:---:|-------|
| **Google Ads** | Search intent, high-intent traffic | ✓ | [google-ads.md](../../tools/integrations/google-ads.md) |
| **Meta Ads** | Demand gen, visual products, B2C | - | [meta-ads.md](../../tools/integrations/meta-ads.md) |
| **LinkedIn Ads** | B2B, job title targeting | - | [linkedin-ads.md](../../tools/integrations/linkedin-ads.md) |
| **TikTok Ads** | Younger demographics, video | - | [tiktok-ads.md](../../tools/integrations/tiktok-ads.md) |
For tracking, see also: [ga4.md](../../tools/integrations/ga4.md), [segment.md](../../tools/integrations/segment.md)
---
## Related Skills
- **ad-creative** — WHEN you need deep creative direction for ad visuals, video scripts, or creative concepting beyond basic image/copy guidelines. NOT for campaign strategy, targeting, or bidding decisions.
- **analytics-tracking** — WHEN setting up conversion tracking pixels, UTM parameters, and attribution models before or during campaign launch. NOT for campaign creation or creative work.
- **campaign-analytics** — WHEN analyzing campaign performance data, diagnosing underperforming campaigns, or building reporting dashboards. NOT for initial campaign setup or creative production.
- **copywriting** — WHEN landing pages linked from ads need copy optimization to match ad messaging and improve post-click conversion. NOT for the ad copy itself.
- **marketing-context** — Foundation skill for ICP, positioning, and messaging alignment. ALWAYS load before writing ad copy or selecting targeting to ensure message-market fit.
---
## Communication
Always confirm conversion tracking is in place before recommending creative or targeting changes — a campaign without proper attribution is guesswork. When recommending budget allocation, state the rationale (testing vs. scaling phase). Deliver ad copy as complete, ready-to-launch sets: headline variants, body copy, and CTA. Proactively flag when a landing page mismatch (ad promise ≠ page promise) is the likely conversion bottleneck. Load `marketing-context` for ICP and positioning before writing any copy.
---
## Proactive Triggers
- User asks why ROAS is dropping → check creative fatigue and ad frequency before adjusting targeting or bids.
- User wants to launch their first paid campaign → run through the pre-launch checklist (conversion tracking, landing page speed, UTMs) before touching creative.
- User mentions high CTR but low conversions → diagnose landing page, not the ad; redirect to `page-cro` or `copywriting` skill.
- User is scaling budget aggressively → warn about algorithm learning phase disruption; recommend 20-30% incremental increases with 3-5 day stabilization windows.
- User asks about B2B lead generation via ads → recommend LinkedIn for job-title targeting and flag that CPL will be higher but lead quality better than Meta for high-ACV products.
---
## Output Artifacts
| Artifact | Description |
|----------|-------------|
| Campaign Architecture | Full account structure with campaign names, ad set targeting, naming conventions, and budget allocation |
| Ad Copy Set | 3 headline variants, body copy, and CTA for each ad format and platform, ready to launch |
| Audience Targeting Brief | Primary audiences, lookalike seeds, retargeting segments, and exclusion lists per platform |
| Pre-Launch Checklist | Platform-specific tracking verification, landing page audit, and UTM parameter setup |
| Weekly Optimization Report Template | Metrics dashboard structure with CPA/ROAS targets, fatigue signals, and decision triggers |
FILE:references/ad-copy-templates.md
# Ad Copy Templates Reference
Detailed formulas and templates for writing high-converting ad copy.
## Primary Text Formulas
### Problem-Agitate-Solve (PAS)
```
[Problem statement]
[Agitate the pain]
[Introduce solution]
[CTA]
```
**Example:**
> Spending hours on manual reporting every week?
> While you're buried in spreadsheets, your competitors are making decisions.
> [Product] automates your reports in minutes.
> Start your free trial →
---
### Before-After-Bridge (BAB)
```
[Current painful state]
[Desired future state]
[Your product as the bridge]
```
**Example:**
> Before: Chasing down approvals across email, Slack, and spreadsheets.
> After: Every approval tracked, automated, and on time.
> [Product] connects your tools and keeps projects moving.
---
### Social Proof Lead
```
[Impressive stat or testimonial]
[What you do]
[CTA]
```
**Example:**
> "We cut our reporting time by 75%." — Sarah K., Marketing Director
> [Product] automates the reports you hate building.
> See how it works →
---
### Feature-Benefit Bridge
```
[Feature]
[So that...]
[Which means...]
```
**Example:**
> Real-time collaboration on documents
> So your team always works from the latest version
> Which means no more version confusion or lost work
---
### Direct Response
```
[Bold claim/outcome]
[Proof point]
[CTA with urgency if genuine]
```
**Example:**
> Cut your reporting time by 80%
> Join 5,000+ marketing teams already using [Product]
> Start free → First month 50% off
---
## Headline Formulas
### For Search Ads
| Formula | Example |
|---------|---------|
| [Keyword] + [Benefit] | "Project Management That Teams Actually Use" |
| [Action] + [Outcome] | "Automate Reports \| Save 10 Hours Weekly" |
| [Question] | "Tired of Manual Data Entry?" |
| [Number] + [Benefit] | "500+ Teams Trust [Product] for [Outcome]" |
| [Keyword] + [Differentiator] | "CRM Built for Small Teams" |
| [Price/Offer] + [Keyword] | "Free Project Management \| No Credit Card" |
### For Social Ads
| Type | Example |
|------|---------|
| Outcome hook | "How we 3x'd our conversion rate" |
| Curiosity hook | "The reporting hack no one talks about" |
| Contrarian hook | "Why we stopped using [common tool]" |
| Specificity hook | "The exact template we use for..." |
| Question hook | "What if you could cut your admin time in half?" |
| Number hook | "7 ways to improve your workflow today" |
| Story hook | "We almost gave up. Then we found..." |
---
## CTA Variations
### Soft CTAs (awareness/consideration)
Best for: Top of funnel, cold audiences, complex products
- Learn More
- See How It Works
- Watch Demo
- Get the Guide
- Explore Features
- See Examples
- Read the Case Study
### Hard CTAs (conversion)
Best for: Bottom of funnel, warm audiences, clear offers
- Start Free Trial
- Get Started Free
- Book a Demo
- Claim Your Discount
- Buy Now
- Sign Up Free
- Get Instant Access
### Urgency CTAs (use when genuine)
Best for: Limited-time offers, scarcity situations
- Limited Time: 30% Off
- Offer Ends [Date]
- Only X Spots Left
- Last Chance
- Early Bird Pricing Ends Soon
### Action-Oriented CTAs
Best for: Active voice, clear next step
- Start Saving Time Today
- Get Your Free Report
- See Your Score
- Calculate Your ROI
- Build Your First Project
---
## Platform-Specific Copy Guidelines
### Google Search Ads
- **Headline limits:** 30 characters each (up to 15 headlines)
- **Description limits:** 90 characters each (up to 4 descriptions)
- Include keywords naturally
- Use all available headline slots
- Include numbers and stats when possible
- Test dynamic keyword insertion
### Meta Ads (Facebook/Instagram)
- **Primary text:** 125 characters visible (can be longer, gets truncated)
- **Headline:** 40 characters recommended
- Front-load the hook (first line matters most)
- Emojis can work but test
- Questions perform well
- Keep image text under 20%
### LinkedIn Ads
- **Intro text:** 600 characters max (150 recommended)
- **Headline:** 200 characters max (70 recommended)
- Professional tone (but not boring)
- Specific job outcomes resonate
- Stats and social proof important
- Avoid consumer-style hype
---
## Copy Testing Priority
When testing ad copy, focus on these elements in order of impact:
1. **Hook/angle** (biggest impact on performance)
2. **Headline**
3. **Primary benefit**
4. **CTA**
5. **Supporting proof points**
Test one element at a time for clean data.
FILE:references/audience-targeting.md
# Audience Targeting Reference
Detailed targeting strategies for each major ad platform.
## Google Ads Audiences
### Search Campaign Targeting
**Keywords:**
- Exact match: [keyword] — most precise, lower volume
- Phrase match: "keyword" — moderate precision and volume
- Broad match: keyword — highest volume, use with smart bidding
**Audience layering:**
- Add audiences in "observation" mode first
- Analyze performance by audience
- Switch to "targeting" mode for high performers
**RLSA (Remarketing Lists for Search Ads):**
- Bid higher on past visitors searching your terms
- Show different ads to returning searchers
- Exclude converters from prospecting campaigns
### Display/YouTube Targeting
**Custom intent audiences:**
- Based on recent search behavior
- Create from your converting keywords
- High intent, good for prospecting
**In-market audiences:**
- People actively researching solutions
- Pre-built by Google
- Layer with demographics for precision
**Affinity audiences:**
- Based on interests and habits
- Better for awareness
- Broad but can exclude irrelevant
**Customer match:**
- Upload email lists
- Retarget existing customers
- Create lookalikes from best customers
**Similar/lookalike audiences:**
- Based on your customer match lists
- Expand reach while maintaining relevance
- Best when source list is high-quality customers
---
## Meta Audiences
### Core Audiences (Interest/Demographic)
**Interest targeting tips:**
- Layer interests with AND logic for precision
- Use Audience Insights to research interests
- Start broad, let algorithm optimize
- Exclude existing customers always
**Demographic targeting:**
- Age and gender (if product-specific)
- Location (down to zip/postal code)
- Language
- Education and work (limited data now)
**Behavior targeting:**
- Purchase behavior
- Device usage
- Travel patterns
- Life events
### Custom Audiences
**Website visitors:**
- All visitors (last 180 days max)
- Specific page visitors
- Time on site thresholds
- Frequency (visited X times)
**Customer list:**
- Upload emails/phone numbers
- Match rate typically 30-70%
- Refresh regularly for accuracy
**Engagement audiences:**
- Video viewers (25%, 50%, 75%, 95%)
- Page/profile engagers
- Form openers
- Instagram engagers
**App activity:**
- App installers
- In-app events
- Purchase events
### Lookalike Audiences
**Source audience quality matters:**
- Use high-LTV customers, not all customers
- Purchasers > leads > all visitors
- Minimum 100 source users, ideally 1,000+
**Size recommendations:**
- 1% — most similar, smallest reach
- 1-3% — good balance for most
- 3-5% — broader, good for scale
- 5-10% — very broad, awareness only
**Layering strategies:**
- Lookalike + interest = more precision early
- Test lookalike-only as you scale
- Exclude the source audience
---
## LinkedIn Audiences
### Job-Based Targeting
**Job titles:**
- Be specific (CMO vs. "Marketing")
- LinkedIn normalizes titles, but verify
- Stack related titles
- Exclude irrelevant titles
**Job functions:**
- Broader than titles
- Combine with seniority level
- Good for awareness campaigns
**Seniority levels:**
- Entry, Senior, Manager, Director, VP, CXO, Partner
- Layer with function for precision
**Skills:**
- Self-reported, less reliable
- Good for technical roles
- Use as expansion layer
### Company-Based Targeting
**Company size:**
- 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5000+
- Key filter for B2B
**Industry:**
- Based on company classification
- Can be broad, layer with other criteria
**Company names (ABM):**
- Upload target account list
- Minimum 300 companies recommended
- Match rate varies
**Company growth rate:**
- Hiring rapidly = budget available
- Good signal for timing
### High-Performing Combinations
| Use Case | Targeting Combination |
|----------|----------------------|
| Enterprise sales | Company size 1000+ + VP/CXO + Industry |
| SMB sales | Company size 11-200 + Manager/Director + Function |
| Developer tools | Skills + Job function + Company type |
| ABM campaigns | Company list + Decision-maker titles |
| Broad awareness | Industry + Seniority + Geography |
---
## Twitter/X Audiences
### Targeting options:
- Follower lookalikes (accounts similar to followers of X)
- Interest categories
- Keywords (in tweets)
- Conversation topics
- Events
- Tailored audiences (your lists)
### Best practices:
- Follower lookalikes of relevant accounts work well
- Keyword targeting catches active conversations
- Lower CPMs than LinkedIn/Meta
- Less precise, better for awareness
---
## TikTok Audiences
### Targeting options:
- Demographics (age, gender, location)
- Interests (TikTok's categories)
- Behaviors (video interactions)
- Device (iOS/Android, connection type)
- Custom audiences (pixel, customer file)
- Lookalike audiences
### Best practices:
- Younger skew (18-34 primarily)
- Interest targeting is broad
- Creative matters more than targeting
- Let algorithm optimize with broad targeting
---
## Audience Size Guidelines
| Platform | Minimum Recommended | Ideal Range |
|----------|-------------------|-------------|
| Google Search | 1,000+ searches/mo | 5,000-50,000 |
| Google Display | 100,000+ | 500K-5M |
| Meta | 100,000+ | 500K-10M |
| LinkedIn | 50,000+ | 100K-500K |
| Twitter/X | 50,000+ | 100K-1M |
| TikTok | 100,000+ | 1M+ |
Too narrow = expensive, slow learning
Too broad = wasted spend, poor relevance
---
## Exclusion Strategy
Always exclude:
- Existing customers (unless upsell)
- Recent converters (7-14 days)
- Bounced visitors (<10 sec)
- Employees (by company or email list)
- Irrelevant page visitors (careers, support)
- Competitors (if identifiable)
FILE:references/platform-setup-checklists.md
# Platform Setup Checklists
Complete setup checklists for major ad platforms.
## Google Ads Setup
### Account Foundation
- [ ] Google Ads account created and verified
- [ ] Billing information added
- [ ] Time zone and currency set correctly
- [ ] Account access granted to team members
### Conversion Tracking
- [ ] Google tag installed on all pages
- [ ] Conversion actions created (purchase, lead, signup)
- [ ] Conversion values assigned (if applicable)
- [ ] Enhanced conversions enabled
- [ ] Test conversions firing correctly
- [ ] Import conversions from GA4 (optional)
### Analytics Integration
- [ ] Google Analytics 4 linked
- [ ] Auto-tagging enabled
- [ ] GA4 audiences available in Google Ads
- [ ] Cross-domain tracking set up (if multiple domains)
### Audience Setup
- [ ] Remarketing tag verified
- [ ] Website visitor audiences created:
- All visitors (180 days)
- Key page visitors (pricing, demo, features)
- Converters (for exclusion)
- [ ] Customer match lists uploaded
- [ ] Similar audiences enabled
### Campaign Readiness
- [ ] Negative keyword lists created:
- Universal negatives (free, jobs, careers, reviews, complaints)
- Competitor negatives (if needed)
- Irrelevant industry terms
- [ ] Location targeting set (include/exclude)
- [ ] Language targeting set
- [ ] Ad schedule configured (if B2B, business hours)
- [ ] Device bid adjustments considered
### Ad Extensions
- [ ] Sitelinks (4-6 relevant pages)
- [ ] Callouts (key benefits, offers)
- [ ] Structured snippets (features, types, services)
- [ ] Call extension (if phone leads valuable)
- [ ] Lead form extension (if using)
- [ ] Price extensions (if applicable)
- [ ] Image extensions (where available)
### Brand Protection
- [ ] Brand campaign running (protect branded terms)
- [ ] Competitor campaigns considered
- [ ] Brand terms in negative lists for non-brand campaigns
---
## Meta Ads Setup
### Business Manager Foundation
- [ ] Business Manager created
- [ ] Business verified (if running certain ad types)
- [ ] Ad account created within Business Manager
- [ ] Payment method added
- [ ] Team access configured with proper roles
### Pixel & Tracking
- [ ] Meta Pixel installed on all pages
- [ ] Standard events configured:
- PageView (automatic)
- ViewContent (product/feature pages)
- Lead (form submissions)
- Purchase (conversions)
- AddToCart (if e-commerce)
- InitiateCheckout (if e-commerce)
- [ ] Conversions API (CAPI) set up for server-side tracking
- [ ] Event Match Quality score > 6
- [ ] Test events in Events Manager
### Domain & Aggregated Events
- [ ] Domain verified in Business Manager
- [ ] Aggregated Event Measurement configured
- [ ] Top 8 events prioritized in order of importance
- [ ] Web events prioritized for iOS 14+ tracking
### Audience Setup
- [ ] Custom audiences created:
- Website visitors (all, 30/60/90/180 days)
- Key page visitors
- Video viewers (25%, 50%, 75%, 95%)
- Page/Instagram engagers
- Customer list uploaded
- [ ] Lookalike audiences created (1%, 1-3%)
- [ ] Saved audiences for common targeting
### Catalog (E-commerce)
- [ ] Product catalog connected
- [ ] Product feed updating correctly
- [ ] Catalog sales campaigns enabled
- [ ] Dynamic product ads configured
### Creative Assets
- [ ] Images in correct sizes:
- Feed: 1080x1080 (1:1)
- Stories/Reels: 1080x1920 (9:16)
- Landscape: 1200x628 (1.91:1)
- [ ] Videos in correct formats
- [ ] Ad copy variations ready
- [ ] UTM parameters in all destination URLs
### Compliance
- [ ] Special Ad Categories declared (if housing, credit, employment, politics)
- [ ] Landing page complies with Meta policies
- [ ] No prohibited content in ads
---
## LinkedIn Ads Setup
### Campaign Manager Foundation
- [ ] Campaign Manager account created
- [ ] Company Page connected
- [ ] Billing information added
- [ ] Team access configured
### Insight Tag & Tracking
- [ ] LinkedIn Insight Tag installed on all pages
- [ ] Tag verified and firing
- [ ] Conversion tracking configured:
- URL-based conversions
- Event-specific conversions
- [ ] Conversion values set (if applicable)
### Audience Setup
- [ ] Matched Audiences created:
- Website retargeting audiences
- Company list uploaded (for ABM)
- Contact list uploaded
- [ ] Lookalike audiences created
- [ ] Saved audiences for common targeting
### Lead Gen Forms (if using)
- [ ] Lead gen form templates created
- [ ] Form fields selected (minimize for conversion)
- [ ] Privacy policy URL added
- [ ] Thank you message configured
- [ ] CRM integration set up (or CSV export process)
### Document Ads (if using)
- [ ] Documents uploaded (PDF, PowerPoint)
- [ ] Gating configured (full gate or preview)
- [ ] Lead gen form connected
### Creative Assets
- [ ] Single image ads: 1200x627 (1.91:1) or 1080x1080 (1:1)
- [ ] Carousel images ready
- [ ] Video specs met (if using)
- [ ] Ad copy within character limits:
- Intro text: 600 max, 150 recommended
- Headline: 200 max, 70 recommended
### Budget Considerations
- [ ] Budget realistic for LinkedIn CPCs ($8-15+ typical)
- [ ] Audience size validated (50K+ recommended)
- [ ] Daily vs. lifetime budget decided
- [ ] Bid strategy selected
---
## Twitter/X Ads Setup
### Account Foundation
- [ ] Ads account created
- [ ] Payment method added
- [ ] Account verified (if required)
### Tracking
- [ ] Twitter Pixel installed
- [ ] Conversion events created
- [ ] Website tag verified
### Audience Setup
- [ ] Tailored audiences created:
- Website visitors
- Customer lists
- [ ] Follower lookalikes identified
- [ ] Interest and keyword targets researched
### Creative
- [ ] Tweet copy within 280 characters
- [ ] Images: 1200x675 (1.91:1) or 1200x1200 (1:1)
- [ ] Video specs met (if using)
- [ ] Cards configured (website, app, etc.)
---
## TikTok Ads Setup
### Account Foundation
- [ ] TikTok Ads Manager account created
- [ ] Business verification completed
- [ ] Payment method added
### Pixel & Tracking
- [ ] TikTok Pixel installed
- [ ] Events configured (ViewContent, Purchase, etc.)
- [ ] Events API set up (recommended)
### Audience Setup
- [ ] Custom audiences created
- [ ] Lookalike audiences created
- [ ] Interest categories identified
### Creative
- [ ] Vertical video (9:16) ready
- [ ] Native-feeling content (not too polished)
- [ ] First 3 seconds are compelling hooks
- [ ] Captions added (most watch without sound)
- [ ] Music/sounds selected (licensed if needed)
---
## Universal Pre-Launch Checklist
Before launching any campaign:
- [ ] Conversion tracking tested with real conversion
- [ ] Landing page loads fast (<3 sec)
- [ ] Landing page mobile-friendly
- [ ] UTM parameters working
- [ ] Budget set correctly (daily vs. lifetime)
- [ ] Start/end dates correct
- [ ] Targeting matches intended audience
- [ ] Ad creative approved
- [ ] Team notified of launch
- [ ] Reporting dashboard ready
FILE:scripts/roas_calculator.py
#!/usr/bin/env python3
"""
roas_calculator.py — ROAS and paid-ads metrics calculator
Usage:
python3 roas_calculator.py --spend 5000 --revenue 18000 --conversions 120 --leads 400 --margin 40
python3 roas_calculator.py --file campaign.json
python3 roas_calculator.py --json # demo + JSON output
python3 roas_calculator.py # demo mode
"""
import argparse
import json
import sys
# ---------------------------------------------------------------------------
# Calculation core
# ---------------------------------------------------------------------------
def calculate(spend: float, revenue: float = 0.0, conversions: int = 0,
leads: int = 0, margin_pct: float = 0.0,
impressions: int = 0, clicks: int = 0) -> dict:
results = {
"inputs": {
"ad_spend": spend,
"revenue": revenue,
"conversions": conversions,
"leads": leads,
"margin_pct": margin_pct,
"impressions": impressions,
"clicks": clicks,
}
}
metrics = {}
# --- ROAS ---
if revenue > 0 and spend > 0:
roas = revenue / spend
metrics["roas"] = {
"value": round(roas, 2),
"formula": "revenue / ad_spend",
"interpretation": _roas_label(roas),
}
# --- Break-even ROAS ---
if margin_pct > 0:
be_roas = 100 / margin_pct
metrics["break_even_roas"] = {
"value": round(be_roas, 2),
"formula": "100 / margin_%",
"note": f"Need {be_roas:.1f}x ROAS to cover ad costs at {margin_pct}% margin",
}
if revenue > 0:
actual_roas = revenue / spend
profitable = actual_roas >= be_roas
metrics["profitability"] = {
"is_profitable": profitable,
"gap": round(actual_roas - be_roas, 2),
"note": "Profitable ✅" if profitable else f"Unprofitable ❌ — need +{be_roas - actual_roas:.2f}x ROAS",
}
# --- CPA ---
if conversions > 0 and spend > 0:
cpa = spend / conversions
metrics["cpa"] = {
"value": round(cpa, 2),
"formula": "ad_spend / conversions",
"unit": "cost per acquisition",
}
if revenue > 0:
rev_per_conversion = revenue / conversions
metrics["revenue_per_conversion"] = {
"value": round(rev_per_conversion, 2),
"roi_per_conversion": round((rev_per_conversion - cpa) / cpa * 100, 1),
}
# --- CPL ---
if leads > 0 and spend > 0:
cpl = spend / leads
metrics["cpl"] = {
"value": round(cpl, 2),
"formula": "ad_spend / leads",
"unit": "cost per lead",
}
if conversions > 0:
lead_to_conv_rate = conversions / leads * 100
metrics["lead_to_conversion_rate"] = {
"value": round(lead_to_conv_rate, 1),
"unit": "%",
}
# --- Conversion rate ---
if clicks > 0 and conversions > 0:
cvr = conversions / clicks * 100
metrics["conversion_rate"] = {
"value": round(cvr, 2),
"unit": "%",
"benchmark": "2-5% typical for paid search",
}
if clicks > 0 and leads > 0:
lcr = leads / clicks * 100
metrics["lead_capture_rate"] = {
"value": round(lcr, 2),
"unit": "%",
}
# --- CTR ---
if impressions > 0 and clicks > 0:
ctr = clicks / impressions * 100
metrics["ctr"] = {
"value": round(ctr, 2),
"unit": "%",
"benchmark": "2-5% for search, 0.1-0.5% for display",
}
cpm = spend / impressions * 1000
metrics["cpm"] = {
"value": round(cpm, 2),
"unit": "cost per 1000 impressions",
}
cpc = spend / clicks
metrics["cpc"] = {
"value": round(cpc, 2),
"unit": "cost per click",
}
results["metrics"] = metrics
results["recommendations"] = _recommendations(metrics, spend, margin_pct)
return results
def _roas_label(roas: float) -> str:
if roas >= 8:
return "Excellent (8x+)"
if roas >= 5:
return "Strong (5-8x)"
if roas >= 3:
return "Good (3-5x)"
if roas >= 2:
return "Acceptable (2-3x) — check margins"
if roas >= 1:
return "Below target (<2x) — likely unprofitable"
return "Losing money (<1x)"
def _recommendations(metrics: dict, spend: float, margin_pct: float) -> list:
recs = []
roas = metrics.get("roas", {}).get("value")
be_roas = metrics.get("break_even_roas", {}).get("value")
if roas and be_roas:
if roas < be_roas:
shortfall = round((be_roas - roas) * spend, 2)
recs.append(f"⚠️ Losing ,.2f/period — pause or restructure campaign immediately")
elif roas < be_roas * 1.5:
recs.append("⚠️ Marginally profitable — optimize creatives and targeting before scaling")
else:
recs.append("✅ Profitable — consider increasing budget or duplicating campaign")
cpa = metrics.get("cpa", {}).get("value")
cpl = metrics.get("cpl", {}).get("value")
cvr = metrics.get("conversion_rate", {}).get("value")
if cvr and cvr < 2:
recs.append(f"⚠️ CVR {cvr}% is low — test new landing pages, headlines, and CTAs")
elif cvr and cvr >= 5:
recs.append(f"✅ Strong CVR {cvr}% — maximize traffic to this funnel")
if cpa and cpl:
l2c = metrics.get("lead_to_conversion_rate", {}).get("value", 0)
if l2c < 10:
recs.append(f"⚠️ Lead-to-close rate {l2c}% is low — review sales qualification or nurture sequence")
ctr = metrics.get("ctr", {}).get("value")
if ctr:
if ctr < 1:
recs.append(f"⚠️ CTR {ctr}% is low — refresh ad copy and audience targeting")
elif ctr >= 5:
recs.append(f"✅ High CTR {ctr}% — strong creative, ensure LP matches ad message")
if not recs:
recs.append("Add more data (margin %, impressions, leads) for actionable recommendations")
return recs
# ---------------------------------------------------------------------------
# Demo data
# ---------------------------------------------------------------------------
DEMO_DATA = {
"spend": 8500,
"revenue": 34200,
"conversions": 142,
"leads": 680,
"margin_pct": 35,
"impressions": 185000,
"clicks": 3700,
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="ROAS calculator — paid ads performance metrics and recommendations."
)
parser.add_argument("--spend", type=float, help="Total ad spend ($)")
parser.add_argument("--revenue", type=float, default=0, help="Total attributed revenue ($)")
parser.add_argument("--conversions", type=int, default=0, help="Number of purchases/conversions")
parser.add_argument("--leads", type=int, default=0, help="Number of leads generated")
parser.add_argument("--margin", type=float, default=0, help="Gross margin %% (e.g. 40)")
parser.add_argument("--impressions", type=int, default=0, help="Total impressions")
parser.add_argument("--clicks", type=int, default=0, help="Total clicks")
parser.add_argument("--file", help="JSON file with campaign data")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if args.file:
with open(args.file, "r") as f:
data = json.load(f)
elif args.spend:
data = {
"spend": args.spend,
"revenue": args.revenue,
"conversions": args.conversions,
"leads": args.leads,
"margin_pct": args.margin,
"impressions": args.impressions,
"clicks": args.clicks,
}
else:
data = DEMO_DATA
if not args.json:
print("No input provided — running in demo mode.\n")
result = calculate(
spend=data.get("spend", 0),
revenue=data.get("revenue", 0),
conversions=data.get("conversions", 0),
leads=data.get("leads", 0),
margin_pct=data.get("margin_pct", 0),
impressions=data.get("impressions", 0),
clicks=data.get("clicks", 0),
)
if args.json:
print(json.dumps(result, indent=2))
return
inp = result["inputs"]
metrics = result["metrics"]
recs = result["recommendations"]
print("=" * 62)
print(" PAID ADS PERFORMANCE REPORT")
print("=" * 62)
print(f" Spend: >10,.2f")
if inp["revenue"]: print(f" Revenue: >10,.2f")
if inp["conversions"]:print(f" Conversions:{inp['conversions']:>10}")
if inp["leads"]: print(f" Leads: {inp['leads']:>10}")
if inp["impressions"]:print(f" Impressions:{inp['impressions']:>10,}")
if inp["clicks"]: print(f" Clicks: {inp['clicks']:>10,}")
print()
print(" METRICS")
print(" " + "─" * 58)
metric_labels = [
("roas", "ROAS", lambda m: f"{m['value']}x — {m['interpretation']}"),
("break_even_roas", "Break-even ROAS", lambda m: f"{m['value']}x — {m['note']}"),
("profitability", "Profitability", lambda m: m['note']),
("cpa", "CPA", lambda m: f",.2f / {m['unit']}"),
("revenue_per_conversion", "Rev/Conversion", lambda m: f",.2f (ROI {m['roi_per_conversion']}%)"),
("cpl", "CPL", lambda m: f",.2f / {m['unit']}"),
("lead_to_conversion_rate","Lead→Conv Rate", lambda m: f"{m['value']}%"),
("conversion_rate", "Conversion Rate", lambda m: f"{m['value']}% ({m['benchmark']})"),
("ctr", "CTR", lambda m: f"{m['value']}%"),
("cpc", "CPC", lambda m: f",.2f"),
("cpm", "CPM", lambda m: f",.2f"),
]
for key, label, fmt in metric_labels:
if key in metrics:
try:
detail = fmt(metrics[key])
print(f" {label:<24} {detail}")
except Exception:
pass
print()
print(" RECOMMENDATIONS")
print(" " + "─" * 58)
for rec in recs:
print(f" {rec}")
print("=" * 62)
if __name__ == "__main__":
main()
When the user wants to optimize, improve, or increase conversions on any marketing page — including homepage, landing pages, pricing pages, feature pages, or...
---
name: "page-cro"
description: When the user wants to optimize, improve, or increase conversions on any marketing page — including homepage, landing pages, pricing pages, feature pages, or blog posts. Also use when the user says "CRO," "conversion rate optimization," "this page isn't converting," "improve conversions," or "why isn't this page working." For signup/registration flows, see signup-flow-cro. For post-signup activation, see onboarding-cro. For forms outside of signup, see form-cro. For popups/modals, see popup-cro.
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Page Conversion Rate Optimization (CRO)
You are a conversion rate optimization expert. Your goal is to analyze marketing pages and provide actionable recommendations to improve conversion rates.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before providing recommendations, identify:
1. **Page Type**: Homepage, landing page, pricing, feature, blog, about, other
2. **Primary Conversion Goal**: Sign up, request demo, purchase, subscribe, download, contact sales
3. **Traffic Context**: Where are visitors coming from? (organic, paid, email, social)
---
## CRO Analysis Framework
Analyze the page across these dimensions, in order of impact:
### 1. Value Proposition Clarity (Highest Impact)
**Check for:**
- Can a visitor understand what this is and why they should care within 5 seconds?
- Is the primary benefit clear, specific, and differentiated?
- Is it written in the customer's language (not company jargon)?
**Common issues:**
- Feature-focused instead of benefit-focused
- Too vague or too clever (sacrificing clarity)
- Trying to say everything instead of the most important thing
### 2. Headline Effectiveness
**Evaluate:**
- Does it communicate the core value proposition?
- Is it specific enough to be meaningful?
- Does it match the traffic source's messaging?
**Strong headline patterns:**
- Outcome-focused: "Get [desired outcome] without [pain point]"
- Specificity: Include numbers, timeframes, or concrete details
- Social proof: "Join 10,000+ teams who..."
### 3. CTA Placement, Copy, and Hierarchy
**Primary CTA assessment:**
- Is there one clear primary action?
- Is it visible without scrolling?
- Does the button copy communicate value, not just action?
- Weak: "Submit," "Sign Up," "Learn More"
- Strong: "Start Free Trial," "Get My Report," "See Pricing"
**CTA hierarchy:**
- Is there a logical primary vs. secondary CTA structure?
- Are CTAs repeated at key decision points?
### 4. Visual Hierarchy and Scannability
**Check:**
- Can someone scanning get the main message?
- Are the most important elements visually prominent?
- Is there enough white space?
- Do images support or distract from the message?
### 5. Trust Signals and Social Proof
**Types to look for:**
- Customer logos (especially recognizable ones)
- Testimonials (specific, attributed, with photos)
- Case study snippets with real numbers
- Review scores and counts
- Security badges (where relevant)
**Placement:** Near CTAs and after benefit claims
### 6. Objection Handling
**Common objections to address:**
- Price/value concerns
- "Will this work for my situation?"
- Implementation difficulty
- "What if it doesn't work?"
**Address through:** FAQ sections, guarantees, comparison content, process transparency
### 7. Friction Points
**Look for:**
- Too many form fields
- Unclear next steps
- Confusing navigation
- Required information that shouldn't be required
- Mobile experience issues
- Long load times
---
## Output Format
Structure your recommendations as:
### Quick Wins (Implement Now)
Easy changes with likely immediate impact.
### High-Impact Changes (Prioritize)
Bigger changes that require more effort but will significantly improve conversions.
### Test Ideas
Hypotheses worth A/B testing rather than assuming.
### Copy Alternatives
For key elements (headlines, CTAs), provide 2-3 alternatives with rationale.
---
## Page-Specific Frameworks
### Homepage CRO
- Clear positioning for cold visitors
- Quick path to most common conversion
- Handle both "ready to buy" and "still researching"
### Landing Page CRO
- Message match with traffic source
- Single CTA (remove navigation if possible)
- Complete argument on one page
### Pricing Page CRO
- Clear plan comparison
- Recommended plan indication
- Address "which plan is right for me?" anxiety
### Feature Page CRO
- Connect feature to benefit
- Use cases and examples
- Clear path to try/buy
### Blog Post CRO
- Contextual CTAs matching content topic
- Inline CTAs at natural stopping points
---
## Experiment Ideas
When recommending experiments, consider tests for:
- Hero section (headline, visual, CTA)
- Trust signals and social proof placement
- Pricing presentation
- Form optimization
- Navigation and UX
**For comprehensive experiment ideas by page type**: See [references/experiments.md](references/experiments.md)
---
## Task-Specific Questions
1. What's your current conversion rate and goal?
2. Where is traffic coming from?
3. What does your signup/purchase flow look like after this page?
4. Do you have user research, heatmaps, or session recordings?
5. What have you already tried?
---
## Related Skills
- **signup-flow-cro** — WHEN: the page itself converts well but users drop off during the signup or registration process that follows it. WHEN NOT: don't switch to signup-flow-cro if the page itself is the bottleneck; fix the page first.
- **form-cro** — WHEN: the page contains a lead capture or contact form that is a conversion point in its own right (not a signup flow). WHEN NOT: don't use for embedded signup/account-creation forms; those belong in signup-flow-cro.
- **popup-cro** — WHEN: a popup or exit-intent modal is being considered as a conversion layer on top of the page. WHEN NOT: don't reach for popups before fixing core page conversion issues.
- **copywriting** — WHEN: the page requires a full copy overhaul, not just CTA tweaks; the messaging architecture needs rebuilding from the value prop down. WHEN NOT: don't invoke copywriting for minor headline or button copy iterations.
- **ab-test-setup** — WHEN: recommendations are ready and the team needs a structured experiment plan to validate changes without guessing. WHEN NOT: don't use ab-test-setup before having a clear hypothesis from the CRO analysis.
- **onboarding-cro** — WHEN: post-conversion activation is the real problem and the page is already converting adequately. WHEN NOT: don't jump to onboarding-cro before confirming the page conversion rate is acceptable.
- **marketing-context** — WHEN: always read `.claude/product-marketing-context.md` first to understand ICP, messaging, and traffic sources before evaluating the page. WHEN NOT: skip if the user has shared all relevant context directly.
---
## Communication
All page CRO output follows this quality standard:
- Recommendations are always organized as **Quick Wins → High-Impact → Test Ideas** — never a flat list
- Every recommendation includes a brief rationale tied to the CRO analysis framework dimension it addresses
- Copy alternatives are provided in sets of 2-3 with the reasoning for each variant
- Page-specific framework (homepage, landing page, pricing, etc.) is applied explicitly — don't give generic advice
- Never recommend A/B testing as a substitute for obvious fixes; call out what to fix vs. what to test
- Avoid prescribing layout without acknowledging traffic source and audience context
---
## Proactive Triggers
Automatically surface page-cro recommendations when:
1. **"This page isn't converting"** — Any mention of low conversion, poor page performance, or high bounce rate immediately activates the CRO analysis framework.
2. **New landing page being built** — When copywriting or frontend-design skills are active and a marketing page is being created, proactively offer a CRO review before launch.
3. **Paid traffic mentioned** — User describes running ads to a page; immediately flag message-match and single-CTA best practices.
4. **Pricing page discussion** — Any pricing strategy or packaging conversation; proactively recommend pricing page CRO review alongside positioning work.
5. **A/B test results reviewed** — When ab-test-setup skill surfaces test results, offer a page-cro analysis to generate the next round of hypotheses.
---
## Output Artifacts
| Artifact | Format | Description |
|----------|--------|-------------|
| CRO Audit Summary | Markdown sections | Analysis across all 7 framework dimensions with issue severity ratings |
| Quick Wins List | Bullet list | ≤5 changes implementable immediately with expected impact |
| High-Impact Recommendations | Structured list | Each with rationale, effort estimate, and success metric |
| Copy Alternatives | Side-by-side table | 2-3 variants per key element (headline, CTA, subhead) with reasoning |
| A/B Test Hypotheses | Table | Hypothesis × variant description × success metric × priority |
FILE:scripts/conversion_audit.py
#!/usr/bin/env python3
"""
conversion_audit.py — CRO audit for HTML pages
Usage:
python3 conversion_audit.py --file page.html
python3 conversion_audit.py --url https://example.com
python3 conversion_audit.py --json
python3 conversion_audit.py # demo mode
"""
import argparse
import json
import re
import sys
import urllib.request
from html.parser import HTMLParser
# ---------------------------------------------------------------------------
# HTML Parser
# ---------------------------------------------------------------------------
class CROParser(HTMLParser):
def __init__(self):
super().__init__()
self._depth = 0
self._above_fold_depth = 3 # approximate first screenful
self._above_fold_elements = 0
self._total_elements = 0
self.buttons = [] # {"text": str, "position": int}
self.links_as_cta = [] # a tags with CTA-like classes/text
self.form_fields = 0
self.forms = 0
# Social proof
self.testimonial_markers = 0
self.logo_images = 0
self.social_numbers = [] # "X customers", "X reviews", etc.
# Trust signals
self.ssl_mentions = 0
self.guarantee_mentions = 0
self.privacy_mentions = 0
# Viewport meta
self.viewport_meta = False
# Tracking state
self._in_body = False
self._above_fold_done = False
self._body_element_count = 0
self._in_script = False
self._in_style = False
self._current_tag = None
self._current_text = []
self._element_position = 0 # rough position counter
# Full text (for regex scans)
self.full_text = []
def handle_starttag(self, tag, attrs):
attrs_dict = dict(attrs)
tag_lower = tag.lower()
if tag_lower == "script":
self._in_script = True
return
if tag_lower == "style":
self._in_style = True
return
if tag_lower == "body":
self._in_body = True
return
if tag_lower == "meta":
if attrs_dict.get("name", "").lower() == "viewport":
self.viewport_meta = True
if not self._in_body:
return
self._element_position += 1
# Buttons
if tag_lower == "button":
self._current_tag = "button"
self._current_text = []
elif tag_lower == "input":
input_type = attrs_dict.get("type", "text").lower()
if input_type == "submit":
val = attrs_dict.get("value", "Submit")
self.buttons.append({"text": val, "position": self._element_position})
elif input_type not in ("hidden", "submit"):
self.form_fields += 1
elif tag_lower == "textarea" or tag_lower == "select":
self.form_fields += 1
elif tag_lower == "form":
self.forms += 1
elif tag_lower == "a":
cls = attrs_dict.get("class", "").lower()
href = attrs_dict.get("href", "")
cta_classes = {"btn", "button", "cta", "call-to-action", "signup", "register"}
if any(c in cls for c in cta_classes):
self._current_tag = "a_cta"
self._current_text = []
elif tag_lower == "img":
src = attrs_dict.get("src", "").lower()
alt = attrs_dict.get("alt", "").lower()
cls = attrs_dict.get("class", "").lower()
if any(kw in src or kw in alt or kw in cls
for kw in ("logo", "partner", "client", "badge", "seal", "award", "cert")):
self.logo_images += 1
def handle_endtag(self, tag):
tag_lower = tag.lower()
if tag_lower == "script":
self._in_script = False
elif tag_lower == "style":
self._in_style = False
elif tag_lower == "button" and self._current_tag == "button":
text = " ".join(self._current_text).strip()
self.buttons.append({"text": text, "position": self._element_position})
self._current_tag = None
self._current_text = []
elif tag_lower == "a" and self._current_tag == "a_cta":
text = " ".join(self._current_text).strip()
self.links_as_cta.append({"text": text, "position": self._element_position})
self._current_tag = None
self._current_text = []
def handle_data(self, data):
if self._in_script or self._in_style:
return
text = data.strip()
if not text:
return
if self._current_tag in ("button", "a_cta"):
self._current_text.append(text)
if self._in_body:
self.full_text.append(text)
# ---------------------------------------------------------------------------
# Text-based signal detection
# ---------------------------------------------------------------------------
TESTIMONIAL_PATTERNS = [
r'\b(testimonial|review|quote|said|says|told us|customer story)\b',
r'[""][^""]{20,}[""]', # quoted text
r'\b\d[\d,]+ (reviews?|customers?|users?|clients?|companies)\b',
r'\bstar[s]?\b.{0,10}\b(rating|review)\b',
r'\b(trustpilot|g2|capterra|clutch)\b',
]
TRUST_PATTERNS = {
"ssl": [r'\b(ssl|https|secure|encrypted|tls|256.bit)\b'],
"guarantee": [r'\b(guarantee|guaranteed|money.back|refund|risk.free|no.risk)\b'],
"privacy": [r'\b(privacy|gdpr|data protection|we never share|no spam|unsubscribe)\b'],
}
CTA_TEXT_PATTERNS = [
r'\b(get started|sign up|try free|start free|buy now|order now|get access|'
r'download|schedule|book|claim|join|subscribe|register|contact us|learn more|'
r'get quote|request demo|start trial|get demo)\b',
]
def scan_text_signals(full_text: str) -> dict:
text_lower = full_text.lower()
testimonials = sum(
len(re.findall(p, text_lower, re.IGNORECASE))
for p in TESTIMONIAL_PATTERNS
)
trust = {}
for key, patterns in TRUST_PATTERNS.items():
trust[key] = sum(len(re.findall(p, text_lower, re.IGNORECASE)) for p in patterns)
cta_text_count = sum(
len(re.findall(p, text_lower, re.IGNORECASE))
for p in CTA_TEXT_PATTERNS
)
return {
"testimonial_signals": min(testimonials, 20),
"trust": trust,
"cta_text_count": cta_text_count,
}
# ---------------------------------------------------------------------------
# Scoring
# ---------------------------------------------------------------------------
def score_category(value, thresholds: list) -> int:
"""thresholds: [(min_value, score), ...] sorted asc. Returns score for first match."""
for min_val, score in sorted(thresholds, reverse=True):
if value >= min_val:
return score
return 0
def audit(html: str) -> dict:
parser = CROParser()
parser.feed(html)
full_text = " ".join(parser.full_text)
text_signals = scan_text_signals(full_text)
all_ctas = parser.buttons + parser.links_as_cta
total_cta_count = len(all_ctas) + text_signals["cta_text_count"]
# --- CTA ---
cta_score = score_category(total_cta_count, [(0, 0), (1, 50), (2, 75), (3, 90), (5, 100)])
cta_above_fold = len([c for c in all_ctas if c["position"] <= 5])
if cta_above_fold >= 1:
cta_score = min(100, cta_score + 10)
# --- Forms ---
if parser.forms == 0:
form_score = 60 # not all pages need forms
form_note = "No form detected (OK if not a lead gen page)"
elif parser.form_fields <= 3:
form_score = 100
form_note = f"{parser.form_fields} field(s) — minimal friction"
elif parser.form_fields <= 5:
form_score = 70
form_note = f"{parser.form_fields} field(s) — consider trimming"
else:
form_score = max(10, 100 - (parser.form_fields - 3) * 10)
form_note = f"{parser.form_fields} field(s) — too many, high friction"
# --- Social proof ---
social_signals = text_signals["testimonial_signals"] + parser.logo_images
social_score = score_category(social_signals, [(0, 0), (1, 40), (2, 65), (4, 85), (6, 100)])
# --- Trust signals ---
trust = text_signals["trust"]
trust_total = sum(min(1, v) for v in trust.values()) # 0-3
trust_score = score_category(trust_total, [(0, 20), (1, 60), (2, 80), (3, 100)])
# --- Viewport meta ---
viewport_score = 100 if parser.viewport_meta else 0
# --- Overall ---
weights = {
"cta": 0.30,
"social_proof": 0.25,
"trust_signals": 0.20,
"forms": 0.15,
"viewport_mobile": 0.10,
}
scores = {
"cta": cta_score,
"social_proof": social_score,
"trust_signals": trust_score,
"forms": form_score,
"viewport_mobile": viewport_score,
}
overall = round(sum(scores[k] * weights[k] for k in weights))
return {
"overall_score": overall,
"categories": {
"cta_buttons": {
"score": cta_score,
"button_count": len(parser.buttons),
"cta_link_count": len(parser.links_as_cta),
"cta_text_count": text_signals["cta_text_count"],
"above_fold_ctas": cta_above_fold,
"weight": "30%",
},
"social_proof": {
"score": social_score,
"testimonial_signals": text_signals["testimonial_signals"],
"logo_badge_images": parser.logo_images,
"total_signals": social_signals,
"weight": "25%",
},
"trust_signals": {
"score": trust_score,
"ssl_mentions": trust["ssl"],
"guarantee_mentions": trust["guarantee"],
"privacy_mentions": trust["privacy"],
"weight": "20%",
},
"forms": {
"score": form_score,
"form_count": parser.forms,
"field_count": parser.form_fields,
"note": form_note,
"weight": "15%",
},
"viewport_mobile": {
"score": viewport_score,
"viewport_meta_present": parser.viewport_meta,
"weight": "10%",
},
},
}
# ---------------------------------------------------------------------------
# Demo HTML
# ---------------------------------------------------------------------------
DEMO_HTML = """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Get Your Free Marketing Audit</title>
</head>
<body>
<header>
<img src="logo.png" alt="Acme Corp logo" class="logo">
<a href="#form" class="btn cta">Get Free Audit</a>
</header>
<section class="hero">
<h1>Stop Wasting Your Ad Budget</h1>
<p>Join 12,400 marketers who cut wasted spend by 35% in 30 days.</p>
<button>Start Free Trial</button>
</section>
<section class="social-proof">
<h2>What Our Customers Say</h2>
<blockquote>"This tool saved us $50,000 in the first quarter." — Sarah M., CMO</blockquote>
<blockquote>"Best investment we made in 2023." — James T., Head of Growth</blockquote>
<p>Rated 4.9/5 on G2 with 2,400+ reviews</p>
<p>Trusted by 500+ companies worldwide</p>
<img src="google-partner.png" alt="Google Partner badge" class="badge">
<img src="trustpilot.png" alt="Trustpilot certified" class="badge">
</section>
<section id="form">
<h2>Get Your Free Audit</h2>
<form>
<input type="text" name="name" placeholder="Your name">
<input type="email" name="email" placeholder="Work email">
<button type="submit">Get My Free Audit</button>
</form>
<p>🔒 SSL secured. We never share your data. Unsubscribe anytime.</p>
<p>30-day money-back guarantee. No risk.</p>
</section>
</body>
</html>"""
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="CRO audit — analyzes an HTML page for conversion signals."
)
parser.add_argument("--file", help="Path to HTML file")
parser.add_argument("--url", help="URL to fetch and analyze")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if args.file:
with open(args.file, "r", encoding="utf-8", errors="replace") as f:
html = f.read()
elif args.url:
with urllib.request.urlopen(args.url, timeout=10) as resp:
html = resp.read().decode("utf-8", errors="replace")
else:
html = DEMO_HTML
if not args.json:
print("No input provided — running in demo mode.\n")
result = audit(html)
if args.json:
print(json.dumps(result, indent=2))
return
cats = result["categories"]
overall = result["overall_score"]
print("=" * 62)
print(f" CRO AUDIT RESULTS Overall Score: {overall}/100")
print("=" * 62)
rows = [
("CTA Buttons", "cta_buttons"),
("Social Proof", "social_proof"),
("Trust Signals", "trust_signals"),
("Forms", "forms"),
("Mobile Viewport", "viewport_mobile"),
]
for label, key in rows:
c = cats[key]
score = c["score"]
weight = c["weight"]
bar_len = round(score / 10)
bar = "█" * bar_len + "░" * (10 - bar_len)
icon = "✅" if score >= 70 else ("⚠️ " if score >= 40 else "❌")
print(f" {icon} {label:<18} [{bar}] {score:>3}/100 (weight {weight})")
print()
# Detail callouts
cta = cats["cta_buttons"]
print(f" CTAs: {cta['button_count']} buttons, {cta['cta_link_count']} CTA links, "
f"{cta['cta_text_count']} CTA text phrases, {cta['above_fold_ctas']} above fold")
sp = cats["social_proof"]
print(f" Social Proof: {sp['testimonial_signals']} testimonial signals, "
f"{sp['logo_badge_images']} logos/badges")
ts = cats["trust_signals"]
print(f" Trust: SSL({ts['ssl_mentions']}) Guarantee({ts['guarantee_mentions']}) "
f"Privacy({ts['privacy_mentions']})")
fm = cats["forms"]
print(f" Forms: {fm['form_count']} form(s), {fm['field_count']} field(s) — {fm['note']}")
print()
grade = "A" if overall >= 85 else "B" if overall >= 70 else "C" if overall >= 55 else "D" if overall >= 40 else "F"
print("=" * 62)
print(f" Grade: {grade} Score: {overall}/100")
print("=" * 62)
if __name__ == "__main__":
main()
When the user wants to optimize post-signup onboarding, user activation, first-run experience, or time-to-value. Also use when the user mentions "onboarding...
---
name: "onboarding-cro"
description: When the user wants to optimize post-signup onboarding, user activation, first-run experience, or time-to-value. Also use when the user mentions "onboarding flow," "activation rate," "user activation," "first-run experience," "empty states," "onboarding checklist," "aha moment," or "new user experience." For signup/registration optimization, see signup-flow-cro. For ongoing email sequences, see email-sequence.
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Onboarding CRO
You are an expert in user onboarding and activation. Your goal is to help users reach their "aha moment" as quickly as possible and establish habits that lead to long-term retention.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before providing recommendations, understand:
1. **Product Context** - What type of product? B2B or B2C? Core value proposition?
2. **Activation Definition** - What's the "aha moment"? What action indicates a user "gets it"?
3. **Current State** - What happens after signup? Where do users drop off?
---
## Core Principles
### 1. Time-to-Value Is Everything
Remove every step between signup and experiencing core value.
### 2. One Goal Per Session
Focus first session on one successful outcome. Save advanced features for later.
### 3. Do, Don't Show
Interactive > Tutorial. Doing the thing > Learning about the thing.
### 4. Progress Creates Motivation
Show advancement. Celebrate completions. Make the path visible.
---
## Defining Activation
### Find Your Aha Moment
The action that correlates most strongly with retention:
- What do retained users do that churned users don't?
- What's the earliest indicator of future engagement?
**Examples by product type:**
- Project management: Create first project + add team member
- Analytics: Install tracking + see first report
- Design tool: Create first design + export/share
- Marketplace: Complete first transaction
### Activation Metrics
- % of signups who reach activation
- Time to activation
- Steps to activation
- Activation by cohort/source
---
## Onboarding Flow Design
### Immediate Post-Signup (First 30 Seconds)
| Approach | Best For | Risk |
|----------|----------|------|
| Product-first | Simple products, B2C, mobile | Blank slate overwhelm |
| Guided setup | Products needing personalization | Adds friction before value |
| Value-first | Products with demo data | May not feel "real" |
**Whatever you choose:**
- Clear single next action
- No dead ends
- Progress indication if multi-step
### Onboarding Checklist Pattern
**When to use:**
- Multiple setup steps required
- Product has several features to discover
- Self-serve B2B products
**Best practices:**
- 3-7 items (not overwhelming)
- Order by value (most impactful first)
- Start with quick wins
- Progress bar/completion %
- Celebration on completion
- Dismiss option (don't trap users)
### Empty States
Empty states are onboarding opportunities, not dead ends.
**Good empty state:**
- Explains what this area is for
- Shows what it looks like with data
- Clear primary action to add first item
- Optional: Pre-populate with example data
### Tooltips and Guided Tours
**When to use:** Complex UI, features that aren't self-evident, power features users might miss
**Best practices:**
- Max 3-5 steps per tour
- Dismissable at any time
- Don't repeat for returning users
---
## Multi-Channel Onboarding
### Email + In-App Coordination
**Trigger-based emails:**
- Welcome email (immediate)
- Incomplete onboarding (24h, 72h)
- Activation achieved (celebration + next step)
- Feature discovery (days 3, 7, 14)
**Email should:**
- Reinforce in-app actions, not duplicate them
- Drive back to product with specific CTA
- Be personalized based on actions taken
---
## Handling Stalled Users
### Detection
Define "stalled" criteria (X days inactive, incomplete setup)
### Re-engagement Tactics
1. **Email sequence** - Reminder of value, address blockers, offer help
2. **In-app recovery** - Welcome back, pick up where left off
3. **Human touch** - For high-value accounts, personal outreach
---
## Measurement
### Key Metrics
| Metric | Description |
|--------|-------------|
| Activation rate | % reaching activation event |
| Time to activation | How long to first value |
| Onboarding completion | % completing setup |
| Day 1/7/30 retention | Return rate by timeframe |
### Funnel Analysis
Track drop-off at each step:
```
Signup → Step 1 → Step 2 → Activation → Retention
100% 80% 60% 40% 25%
```
Identify biggest drops and focus there.
---
## Output Format
### Onboarding Audit
For each issue: Finding → Impact → Recommendation → Priority
### Onboarding Flow Design
- Activation goal
- Step-by-step flow
- Checklist items (if applicable)
- Empty state copy
- Email sequence triggers
- Metrics plan
---
## Common Patterns by Product Type
| Product Type | Key Steps |
|--------------|-----------|
| B2B SaaS | Setup wizard → First value action → Team invite → Deep setup |
| Marketplace | Complete profile → Browse → First transaction → Repeat loop |
| Mobile App | Permissions → Quick win → Push setup → Habit loop |
| Content Platform | Follow/customize → Consume → Create → Engage |
---
## Experiment Ideas
When recommending experiments, consider tests for:
- Flow simplification (step count, ordering)
- Progress and motivation mechanics
- Personalization by role or goal
- Support and help availability
**For comprehensive experiment ideas**: See [references/experiments.md](references/experiments.md)
---
## Task-Specific Questions
1. What action most correlates with retention?
2. What happens immediately after signup?
3. Where do users currently drop off?
4. What's your activation rate target?
5. Do you have cohort analysis on successful vs. churned users?
---
## Related Skills
- **signup-flow-cro** — WHEN optimizing the registration and pre-onboarding flow before users ever land in-app. NOT when users have already signed up and activation is the goal.
- **popup-cro** — WHEN using in-product modals, tooltips, or overlays as part of the onboarding experience. NOT for standalone lead capture or exit-intent popups on the marketing site.
- **paywall-upgrade-cro** — WHEN onboarding naturally leads into an upgrade prompt after the aha moment is reached. NOT during early onboarding before value is delivered.
- **ab-test-setup** — WHEN running controlled experiments on onboarding flows, checklists, or step ordering. NOT for initial brainstorming or design.
- **marketing-context** — Foundation skill. ALWAYS load when product/ICP context is needed for personalized onboarding recommendations. NOT optional — load before this skill if available.
---
## Communication
Deliver recommendations following the output quality standard: lead with the highest-leverage finding, provide a clear activation definition, then prioritize experiments by expected impact. Avoid vague advice — every recommendation should name a specific onboarding step, metric, or trigger. When writing onboarding copy or flows, ensure tone matches the product's brand voice (load `marketing-context` if available).
---
## Proactive Triggers
- User mentions low Day-1 or Day-7 retention → immediately ask about their activation event and current post-signup flow.
- User shares a signup funnel with a big drop between "signup" and "first key action" → diagnose onboarding, not acquisition.
- User says "users sign up but don't come back" → frame this as an activation/onboarding problem, not a marketing problem.
- User asks about improving trial-to-paid conversion → check whether activation is defined and being reached before assuming pricing is the blocker.
- User mentions "onboarding emails aren't working" → ask what in-app onboarding exists first; email should support, not replace, in-app experience.
---
## Output Artifacts
| Artifact | Description |
|----------|-------------|
| Activation Definition Doc | Clearly defined aha moment, correlated action, and success metric |
| Onboarding Flow Diagram | Step-by-step post-signup flow with drop-off points and decision branches |
| Checklist Copy | 3–7 onboarding checklist items ordered by value, with completion messaging |
| Email Trigger Map | Trigger conditions, timing, and goals for each onboarding email in the sequence |
| Experiment Backlog | Prioritized A/B test ideas for onboarding steps, sorted by expected impact |
FILE:scripts/activation_funnel_analyzer.py
#!/usr/bin/env python3
"""
Activation Funnel Analyzer for Onboarding CRO
Analyzes user onboarding funnel data to identify drop-off points
and estimate the impact of improving each step.
Usage:
python3 activation_funnel_analyzer.py # Demo mode
python3 activation_funnel_analyzer.py funnel.json # From data
python3 activation_funnel_analyzer.py funnel.json --json # JSON output
Input format (JSON):
{
"steps": [
{"name": "Signup completed", "users": 1000},
{"name": "Email verified", "users": 850},
{"name": "Profile setup", "users": 620},
{"name": "First action", "users": 310},
{"name": "Aha moment", "users": 180},
{"name": "Activated (Day 7)", "users": 120}
]
}
"""
import json
import sys
import os
def analyze_funnel(data):
"""Analyze onboarding funnel for drop-offs and improvement potential."""
steps = data["steps"]
if len(steps) < 2:
return {"error": "Need at least 2 funnel steps"}
total_start = steps[0]["users"]
analysis = []
worst_step = None
worst_drop = 0
for i in range(len(steps)):
step = steps[i]
users = step["users"]
rate_from_start = (users / total_start * 100) if total_start > 0 else 0
if i == 0:
step_analysis = {
"step": step["name"],
"users": users,
"rate_from_start": round(rate_from_start, 1),
"drop_rate": 0,
"dropped_users": 0,
"is_worst": False
}
else:
prev_users = steps[i - 1]["users"]
dropped = prev_users - users
drop_rate = (dropped / prev_users * 100) if prev_users > 0 else 0
step_analysis = {
"step": step["name"],
"users": users,
"rate_from_start": round(rate_from_start, 1),
"drop_rate": round(drop_rate, 1),
"dropped_users": dropped,
"is_worst": False
}
if drop_rate > worst_drop:
worst_drop = drop_rate
worst_step = i
analysis.append(step_analysis)
if worst_step is not None:
analysis[worst_step]["is_worst"] = True
# Calculate improvement potential
final_users = steps[-1]["users"]
overall_conversion = (final_users / total_start * 100) if total_start > 0 else 0
improvements = []
if worst_step is not None:
worst = analysis[worst_step]
# What if we halved the drop-off at the worst step?
current_drop_rate = worst["drop_rate"] / 100
improved_drop_rate = current_drop_rate / 2
prev_users = steps[worst_step - 1]["users"]
gained_users = int(prev_users * (current_drop_rate - improved_drop_rate))
# Propagate improvement through remaining steps
cascade_rate = 1.0
for j in range(worst_step + 1, len(steps)):
if steps[j - 1]["users"] > 0:
cascade_rate *= steps[j]["users"] / steps[j - 1]["users"]
additional_activated = int(gained_users * cascade_rate)
improvements.append({
"action": f"Halve drop-off at '{worst['step']}'",
"current_drop": f"{worst['drop_rate']}%",
"target_drop": f"{worst['drop_rate'] / 2:.1f}%",
"users_saved": gained_users,
"additional_activated": additional_activated,
"impact_on_overall": f"+{(additional_activated / total_start * 100):.1f}pp"
})
# Score
score = min(100, max(0, int(overall_conversion * 5))) # 20% activation = 100
if overall_conversion < 5:
score = max(0, int(overall_conversion * 10))
return {
"steps": analysis,
"summary": {
"total_start": total_start,
"total_activated": final_users,
"overall_conversion": round(overall_conversion, 1),
"worst_step": analysis[worst_step]["step"] if worst_step else None,
"worst_drop_rate": round(worst_drop, 1),
"score": score
},
"improvements": improvements
}
def format_report(result):
"""Format human-readable report."""
lines = []
lines.append("")
lines.append("=" * 65)
lines.append(" ONBOARDING FUNNEL — ACTIVATION ANALYSIS")
lines.append("=" * 65)
lines.append("")
summary = result["summary"]
score = summary["score"]
bar = "█" * (score // 5) + "░" * (20 - score // 5)
lines.append(f" ACTIVATION SCORE: {score}/100")
lines.append(f" [{bar}]")
lines.append(f" Overall: {summary['total_start']} → {summary['total_activated']} ({summary['overall_conversion']}%)")
lines.append("")
# Funnel visualization
lines.append(" FUNNEL:")
max_users = result["steps"][0]["users"]
for step in result["steps"]:
bar_width = int(step["users"] / max_users * 40) if max_users > 0 else 0
bar_char = "█" * bar_width
marker = " ← WORST DROP" if step["is_worst"] else ""
drop_info = f" (-{step['drop_rate']}%)" if step["drop_rate"] > 0 else ""
lines.append(f" {bar_char} {step['users']:>5} | {step['step']}{drop_info}{marker}")
lines.append("")
# Step-by-step breakdown
lines.append(" STEP BREAKDOWN:")
lines.append(f" {'Step':<25} {'Users':>7} {'From Start':>12} {'Drop':>8} {'Lost':>7}")
lines.append(" " + "-" * 62)
for step in result["steps"]:
drop = f"-{step['drop_rate']}%" if step["drop_rate"] > 0 else "—"
lost = f"-{step['dropped_users']}" if step["dropped_users"] > 0 else "—"
lines.append(f" {step['step']:<25} {step['users']:>7} {step['rate_from_start']:>10.1f}% {drop:>8} {lost:>7}")
lines.append("")
# Improvement potential
if result["improvements"]:
lines.append(" 💡 IMPROVEMENT POTENTIAL:")
for imp in result["improvements"]:
lines.append(f" Action: {imp['action']}")
lines.append(f" Drop: {imp['current_drop']} → {imp['target_drop']}")
lines.append(f" Users saved at step: +{imp['users_saved']}")
lines.append(f" Additional activated: +{imp['additional_activated']}")
lines.append(f" Impact on overall rate: {imp['impact_on_overall']}")
lines.append("")
return "\n".join(lines)
SAMPLE_DATA = {
"steps": [
{"name": "Signup completed", "users": 1000},
{"name": "Email verified", "users": 840},
{"name": "Profile setup", "users": 580},
{"name": "First project created", "users": 290},
{"name": "Invited teammate", "users": 145},
{"name": "Aha moment (Day 3)", "users": 95},
{"name": "Activated (Day 7)", "users": 72}
]
}
def main():
use_json = "--json" in sys.argv
args = [a for a in sys.argv[1:] if a != "--json"]
if args and os.path.isfile(args[0]):
with open(args[0]) as f:
data = json.load(f)
else:
if not args:
print("[Demo mode — analyzing sample SaaS onboarding funnel]")
data = SAMPLE_DATA
result = analyze_funnel(data)
if use_json:
print(json.dumps(result, indent=2))
else:
print(format_report(result))
if __name__ == "__main__":
main()
When the user wants to apply psychological principles, mental models, or behavioral science to marketing. Also use when the user mentions 'psychology,' 'ment...
---
name: "marketing-psychology"
description: "When the user wants to apply psychological principles, mental models, or behavioral science to marketing. Also use when the user mentions 'psychology,' 'mental models,' 'cognitive bias,' 'persuasion,' 'behavioral science,' 'why people buy,' 'decision-making,' or 'consumer behavior.' This skill provides 70+ mental models organized for marketing application."
license: MIT
metadata:
version: 1.1.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Marketing Psychology
You are an expert in applied behavioral science for marketing. Your job is to identify which psychological principles apply to a specific marketing challenge and show how to use them — not just name-drop biases.
## Before Starting
**Check for marketing context first:**
If `marketing-context.md` exists, read it for audience personas and product positioning. Psychology works better when you know the audience.
## How This Skill Works
### Mode 1: Diagnose — Why Isn't This Converting?
Analyze a page, flow, or campaign through a behavioral science lens. Identify which cognitive biases or principles are being violated or underutilized.
### Mode 2: Apply — Use Psychology to Improve
Given a specific marketing asset, recommend 3-5 psychological principles to apply with concrete implementation examples.
### Mode 3: Reference — Look Up a Principle
Explain a specific mental model, bias, or principle with marketing applications and examples.
---
## The 70+ Mental Models
The full catalog lives in [references/mental-models-catalog.md](references/mental-models-catalog.md). Load it when you need to look up specific models or browse the full list.
### Categories at a Glance
| Category | Count | Key Models | Marketing Application |
|----------|-------|------------|----------------------|
| **Foundational Thinking** | 14 | First Principles, Jobs to Be Done, Inversion, Pareto, Second-Order Thinking | Strategic decisions, positioning |
| **Buyer Psychology** | 17 | Endowment Effect, Zero-Price Effect, Paradox of Choice, Social Proof | Conversion optimization, pricing |
| **Persuasion & Influence** | 13 | Reciprocity, Scarcity, Loss Aversion, Anchoring, Decoy Effect | Copy, CTAs, offers |
| **Pricing Psychology** | 5 | Charm Pricing, Rule of 100, Good-Better-Best | Pricing pages, discount framing |
| **Design & Delivery** | 10 | AIDA, Hick's Law, Nudge Theory, Fogg Model | UX, onboarding, form design |
| **Growth & Scaling** | 8 | Network Effects, Flywheel, Switching Costs, Compounding | Growth strategy, retention |
### Most-Used Models (start here)
**For conversion optimization:**
- **Loss Aversion** — People feel losses 2x more than gains. Frame benefits as what they'll miss.
- **Anchoring** — First number seen sets expectations. Show higher price first, then your price.
- **Social Proof** — People follow others. Show customer count, testimonials, logos.
- **Scarcity** — Limited availability increases desire. But only if real — fake urgency backfires.
- **Paradox of Choice** — Too many options = no decision. Limit to 3 tiers.
**For pricing:**
- **Charm Pricing** — $49 feels meaningfully cheaper than $50 (left-digit effect).
- **Decoy Effect** — Add a dominated option to make your target tier look like the obvious choice.
- **Rule of 100** — Under $100: show % discount. Over $100: show $ discount.
**For copy and messaging:**
- **Reciprocity** — Give value first (free tool, guide, audit). People feel compelled to reciprocate.
- **Endowment Effect** — Let people "own" something before paying (free trial, saved progress).
- **Framing** — Same fact, different frame. "95% uptime" vs "down 18 days/year." Choose wisely.
---
## Quick Reference
| Situation | Models to Apply |
|-----------|----------------|
| Landing page not converting | Loss Aversion, Social Proof, Anchoring, Hick's Law |
| Pricing page optimization | Charm Pricing, Decoy Effect, Good-Better-Best, Anchoring |
| Email sequence engagement | Reciprocity, Zeigarnik Effect, Goal-Gradient, Commitment |
| Reducing churn | Endowment Effect, Sunk Cost, Switching Costs, Status-Quo Bias |
| Onboarding activation | IKEA Effect, Goal-Gradient, Fogg Model, Default Effect |
| Ad creative improvement | Mere Exposure, Pratfall Effect, Contrast Effect, Framing |
| Referral program design | Reciprocity, Social Proof, Network Effects, Unity Principle |
## Task-Specific Questions
When applying psychology to a specific challenge, ask:
1. **What's the desired behavior?** (Click, buy, share, return?)
2. **What's the current friction?** (Too many choices, unclear value, no urgency?)
3. **What's the emotional state?** (Excited, skeptical, confused, impatient?)
4. **What's the context?** (First visit, returning user, comparing options?)
5. **What's the risk tolerance?** (High-stakes B2B? Low-stakes consumer impulse?)
## Proactive Triggers
- **Landing page has no social proof** → Missing one of the most powerful conversion levers. Add testimonials, customer count, or logos.
- **Pricing page shows all features equally** → No anchoring or decoy. Restructure tiers with a recommended option.
- **CTA uses weak language** → "Submit" or "Get started" vs "Start my free trial" (endowment framing).
- **Too many form fields** → Hick's Law: more choices = more friction. Reduce or use progressive disclosure.
- **No urgency element** → If legitimate scarcity exists, surface it. Countdown timers, limited spots, seasonal offers.
## Output Artifacts
| When you ask for... | You get... |
|---------------------|------------|
| "Why isn't this converting?" | Behavioral diagnosis: which principles are violated + specific fixes |
| "Apply psychology to this page" | 3-5 applicable principles with concrete implementation |
| "Explain [principle]" | Definition + marketing applications + before/after examples |
| "Pricing psychology audit" | Pricing page analysis with principle-by-principle recommendations |
| "Psychology playbook for [goal]" | Curated set of 5-7 models specific to the goal |
## Communication
All output passes quality verification:
- Self-verify: source attribution, assumption audit, confidence scoring
- Output format: Bottom Line → What (with confidence) → Why → How to Act
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Related Skills
- **page-cro**: For full page optimization. Psychology provides the behavioral layer.
- **copywriting**: For writing copy. Psychology informs the persuasion techniques.
- **pricing-strategy**: For pricing decisions. Psychology provides the buyer behavior lens.
- **marketing-context**: Foundation — understanding audience makes psychology more precise.
- **ab-test-setup**: For testing which psychological approach works. Data beats theory.
FILE:references/mental-models-catalog.md
# Mental Models Catalog for Marketing
## Foundational Thinking Models
These models sharpen your strategy and help you solve the right problems.
### First Principles
Break problems down to basic truths and build solutions from there. Instead of copying competitors, ask "why" repeatedly to find root causes. Use the 5 Whys technique to tunnel down to what really matters.
**Marketing application**: Don't assume you need content marketing because competitors do. Ask why you need it, what problem it solves, and whether there's a better solution.
### Jobs to Be Done
People don't buy products—they "hire" them to get a job done. Focus on the outcome customers want, not features.
**Marketing application**: A drill buyer doesn't want a drill—they want a hole. Frame your product around the job it accomplishes, not its specifications.
### Circle of Competence
Know what you're good at and stay within it. Venture outside only with proper learning or expert help.
**Marketing application**: Don't chase every channel. Double down where you have genuine expertise and competitive advantage.
### Inversion
Instead of asking "How do I succeed?", ask "What would guarantee failure?" Then avoid those things.
**Marketing application**: List everything that would make your campaign fail—confusing messaging, wrong audience, slow landing page—then systematically prevent each.
### Occam's Razor
The simplest explanation is usually correct. Avoid overcomplicating strategies or attributing results to complex causes when simple ones suffice.
**Marketing application**: If conversions dropped, check the obvious first (broken form, page speed) before assuming complex attribution issues.
### Pareto Principle (80/20 Rule)
Roughly 80% of results come from 20% of efforts. Identify and focus on the vital few.
**Marketing application**: Find the 20% of channels, customers, or content driving 80% of results. Cut or reduce the rest.
### Local vs. Global Optima
A local optimum is the best solution nearby, but a global optimum is the best overall. Don't get stuck optimizing the wrong thing.
**Marketing application**: Optimizing email subject lines (local) won't help if email isn't the right channel (global). Zoom out before zooming in.
### Theory of Constraints
Every system has one bottleneck limiting throughput. Find and fix that constraint before optimizing elsewhere.
**Marketing application**: If your funnel converts well but traffic is low, more conversion optimization won't help. Fix the traffic bottleneck first.
### Opportunity Cost
Every choice has a cost—what you give up by not choosing alternatives. Consider what you're saying no to.
**Marketing application**: Time spent on a low-ROI channel is time not spent on high-ROI activities. Always compare against alternatives.
### Law of Diminishing Returns
After a point, additional investment yields progressively smaller gains.
**Marketing application**: The 10th blog post won't have the same impact as the first. Know when to diversify rather than double down.
### Second-Order Thinking
Consider not just immediate effects, but the effects of those effects.
**Marketing application**: A flash sale boosts revenue (first order) but may train customers to wait for discounts (second order).
### Map ≠ Territory
Models and data represent reality but aren't reality itself. Don't confuse your analytics dashboard with actual customer experience.
**Marketing application**: Your customer persona is a useful model, but real customers are more complex. Stay in touch with actual users.
### Probabilistic Thinking
Think in probabilities, not certainties. Estimate likelihoods and plan for multiple outcomes.
**Marketing application**: Don't bet everything on one campaign. Spread risk and plan for scenarios where your primary strategy underperforms.
### Barbell Strategy
Combine extreme safety with small high-risk/high-reward bets. Avoid the mediocre middle.
**Marketing application**: Put 80% of budget into proven channels, 20% into experimental bets. Avoid moderate-risk, moderate-reward middle.
---
## Understanding Buyers & Human Psychology
These models explain how customers think, decide, and behave.
### Fundamental Attribution Error
People attribute others' behavior to character, not circumstances. "They didn't buy because they're not serious" vs. "The checkout was confusing."
**Marketing application**: When customers don't convert, examine your process before blaming them. The problem is usually situational, not personal.
### Mere Exposure Effect
People prefer things they've seen before. Familiarity breeds liking.
**Marketing application**: Consistent brand presence builds preference over time. Repetition across channels creates comfort and trust.
### Availability Heuristic
People judge likelihood by how easily examples come to mind. Recent or vivid events seem more common.
**Marketing application**: Case studies and testimonials make success feel more achievable. Make positive outcomes easy to imagine.
### Confirmation Bias
People seek information confirming existing beliefs and ignore contradictory evidence.
**Marketing application**: Understand what your audience already believes and align messaging accordingly. Fighting beliefs head-on rarely works.
### The Lindy Effect
The longer something has survived, the longer it's likely to continue. Old ideas often outlast new ones.
**Marketing application**: Proven marketing principles (clear value props, social proof) outlast trendy tactics. Don't abandon fundamentals for fads.
### Mimetic Desire
People want things because others want them. Desire is socially contagious.
**Marketing application**: Show that desirable people want your product. Waitlists, exclusivity, and social proof trigger mimetic desire.
### Sunk Cost Fallacy
People continue investing in something because of past investment, even when it's no longer rational.
**Marketing application**: Know when to kill underperforming campaigns. Past spend shouldn't justify future spend if results aren't there.
### Endowment Effect
People value things more once they own them.
**Marketing application**: Free trials, samples, and freemium models let customers "own" the product, making them reluctant to give it up.
### IKEA Effect
People value things more when they've put effort into creating them.
**Marketing application**: Let customers customize, configure, or build something. Their investment increases perceived value and commitment.
### Zero-Price Effect
Free isn't just a low price—it's psychologically different. "Free" triggers irrational preference.
**Marketing application**: Free tiers, free trials, and free shipping have disproportionate appeal. The jump from $1 to $0 is bigger than $2 to $1.
### Hyperbolic Discounting / Present Bias
People strongly prefer immediate rewards over future ones, even when waiting is more rational.
**Marketing application**: Emphasize immediate benefits ("Start saving time today") over future ones ("You'll see ROI in 6 months").
### Status-Quo Bias
People prefer the current state of affairs. Change requires effort and feels risky.
**Marketing application**: Reduce friction to switch. Make the transition feel safe and easy. "Import your data in one click."
### Default Effect
People tend to accept pre-selected options. Defaults are powerful.
**Marketing application**: Pre-select the plan you want customers to choose. Opt-out beats opt-in for subscriptions (ethically applied).
### Paradox of Choice
Too many options overwhelm and paralyze. Fewer choices often lead to more decisions.
**Marketing application**: Limit options. Three pricing tiers beat seven. Recommend a single "best for most" option.
### Goal-Gradient Effect
People accelerate effort as they approach a goal. Progress visualization motivates action.
**Marketing application**: Show progress bars, completion percentages, and "almost there" messaging to drive completion.
### Peak-End Rule
People judge experiences by the peak (best or worst moment) and the end, not the average.
**Marketing application**: Design memorable peaks (surprise upgrades, delightful moments) and strong endings (thank you pages, follow-up emails).
### Zeigarnik Effect
Unfinished tasks occupy the mind more than completed ones. Open loops create tension.
**Marketing application**: "You're 80% done" creates pull to finish. Incomplete profiles, abandoned carts, and cliffhangers leverage this.
### Pratfall Effect
Competent people become more likable when they show a small flaw. Perfection is less relatable.
**Marketing application**: Admitting a weakness ("We're not the cheapest, but...") can increase trust and differentiation.
### Curse of Knowledge
Once you know something, you can't imagine not knowing it. Experts struggle to explain simply.
**Marketing application**: Your product seems obvious to you but confusing to newcomers. Test copy with people unfamiliar with your space.
### Mental Accounting
People treat money differently based on its source or intended use, even though money is fungible.
**Marketing application**: Frame costs in favorable mental accounts. "$3/day" feels different than "$90/month" even though it's the same.
### Regret Aversion
People avoid actions that might cause regret, even if the expected outcome is positive.
**Marketing application**: Address regret directly. Money-back guarantees, free trials, and "no commitment" messaging reduce regret fear.
### Bandwagon Effect / Social Proof
People follow what others are doing. Popularity signals quality and safety.
**Marketing application**: Show customer counts, testimonials, logos, reviews, and "trending" indicators. Numbers create confidence.
---
## Influencing Behavior & Persuasion
These models help you ethically influence customer decisions.
### Reciprocity Principle
People feel obligated to return favors. Give first, and people want to give back.
**Marketing application**: Free content, free tools, and generous free tiers create reciprocal obligation. Give value before asking for anything.
### Commitment & Consistency
Once people commit to something, they want to stay consistent with that commitment.
**Marketing application**: Get small commitments first (email signup, free trial). People who've taken one step are more likely to take the next.
### Authority Bias
People defer to experts and authority figures. Credentials and expertise create trust.
**Marketing application**: Feature expert endorsements, certifications, "featured in" logos, and thought leadership content.
### Liking / Similarity Bias
People say yes to those they like and those similar to themselves.
**Marketing application**: Use relatable spokespeople, founder stories, and community language. "Built by marketers for marketers" signals similarity.
### Unity Principle
Shared identity drives influence. "One of us" is powerful.
**Marketing application**: Position your brand as part of the customer's tribe. Use insider language and shared values.
### Scarcity / Urgency Heuristic
Limited availability increases perceived value. Scarcity signals desirability.
**Marketing application**: Limited-time offers, low-stock warnings, and exclusive access create urgency. Only use when genuine.
### Foot-in-the-Door Technique
Start with a small request, then escalate. Compliance with small requests leads to compliance with larger ones.
**Marketing application**: Free trial → paid plan → annual plan → enterprise. Each step builds on the last.
### Door-in-the-Face Technique
Start with an unreasonably large request, then retreat to what you actually want. The contrast makes the second request seem reasonable.
**Marketing application**: Show enterprise pricing first, then reveal the affordable starter plan. The contrast makes it feel like a deal.
### Loss Aversion / Prospect Theory
Losses feel roughly twice as painful as equivalent gains feel good. People will work harder to avoid losing than to gain.
**Marketing application**: Frame in terms of what they'll lose by not acting. "Don't miss out" beats "You could gain."
### Anchoring Effect
The first number people see heavily influences subsequent judgments.
**Marketing application**: Show the higher price first (original price, competitor price, enterprise tier) to anchor expectations.
### Decoy Effect
Adding a third, inferior option makes one of the original two look better.
**Marketing application**: A "decoy" pricing tier that's clearly worse value makes your preferred tier look like the obvious choice.
### Framing Effect
How something is presented changes how it's perceived. Same facts, different frames.
**Marketing application**: "90% success rate" vs. "10% failure rate" are identical but feel different. Frame positively.
### Contrast Effect
Things seem different depending on what they're compared to.
**Marketing application**: Show the "before" state clearly. The contrast with your "after" makes improvements vivid.
---
## Pricing Psychology
These models specifically address how people perceive and respond to prices.
### Charm Pricing / Left-Digit Effect
Prices ending in 9 seem significantly lower than the next round number. $99 feels much cheaper than $100.
**Marketing application**: Use .99 or .95 endings for value-focused products. The left digit dominates perception.
### Rounded-Price (Fluency) Effect
Round numbers feel premium and are easier to process. $100 signals quality; $99 signals value.
**Marketing application**: Use round prices for premium products ($500/month), charm prices for value products ($497/month).
### Rule of 100
For prices under $100, percentage discounts seem larger ("20% off"). For prices over $100, absolute discounts seem larger ("$50 off").
**Marketing application**: $80 product: "20% off" beats "$16 off." $500 product: "$100 off" beats "20% off."
### Price Relativity / Good-Better-Best
People judge prices relative to options presented. A middle tier seems reasonable between cheap and expensive.
**Marketing application**: Three tiers where the middle is your target. The expensive tier makes it look reasonable; the cheap tier provides an anchor.
### Mental Accounting (Pricing)
Framing the same price differently changes perception.
**Marketing application**: "$1/day" feels cheaper than "$30/month." "Less than your morning coffee" reframes the expense.
---
## Design & Delivery Models
These models help you design effective marketing systems.
### Hick's Law
Decision time increases with the number and complexity of choices. More options = slower decisions = more abandonment.
**Marketing application**: Simplify choices. One clear CTA beats three. Fewer form fields beat more.
### AIDA Funnel
Attention → Interest → Desire → Action. The classic customer journey model.
**Marketing application**: Structure pages and campaigns to move through each stage. Capture attention before building desire.
### Rule of 7
Prospects need roughly 7 touchpoints before converting. One ad rarely converts; sustained presence does.
**Marketing application**: Build multi-touch campaigns across channels. Retargeting, email sequences, and consistent presence compound.
### Nudge Theory / Choice Architecture
Small changes in how choices are presented significantly influence decisions.
**Marketing application**: Default selections, strategic ordering, and friction reduction guide behavior without restricting choice.
### BJ Fogg Behavior Model
Behavior = Motivation × Ability × Prompt. All three must be present for action.
**Marketing application**: High motivation but hard to do = won't happen. Easy to do but no prompt = won't happen. Design for all three.
### EAST Framework
Make desired behaviors: Easy, Attractive, Social, Timely.
**Marketing application**: Reduce friction (easy), make it appealing (attractive), show others doing it (social), ask at the right moment (timely).
### COM-B Model
Behavior requires: Capability, Opportunity, Motivation.
**Marketing application**: Can they do it (capability)? Is the path clear (opportunity)? Do they want to (motivation)? Address all three.
### Activation Energy
The initial energy required to start something. High activation energy prevents action even if the task is easy overall.
**Marketing application**: Reduce starting friction. Pre-fill forms, offer templates, show quick wins. Make the first step trivially easy.
### North Star Metric
One metric that best captures the value you deliver to customers. Focus creates alignment.
**Marketing application**: Identify your North Star (active users, completed projects, revenue per customer) and align all efforts toward it.
### The Cobra Effect
When incentives backfire and produce the opposite of intended results.
**Marketing application**: Test incentive structures. A referral bonus might attract low-quality referrals gaming the system.
---
## Growth & Scaling Models
These models explain how marketing compounds and scales.
### Feedback Loops
Output becomes input, creating cycles. Positive loops accelerate growth; negative loops create decline.
**Marketing application**: Build virtuous cycles: more users → more content → better SEO → more users. Identify and strengthen positive loops.
### Compounding
Small, consistent gains accumulate into large results over time. Early gains matter most.
**Marketing application**: Consistent content, SEO, and brand building compound. Start early; benefits accumulate exponentially.
### Network Effects
A product becomes more valuable as more people use it.
**Marketing application**: Design features that improve with more users: shared workspaces, integrations, marketplaces, communities.
### Flywheel Effect
Sustained effort creates momentum that eventually maintains itself. Hard to start, easy to maintain.
**Marketing application**: Content → traffic → leads → customers → case studies → more content. Each element powers the next.
### Switching Costs
The price (time, money, effort, data) of changing to a competitor. High switching costs create retention.
**Marketing application**: Increase switching costs ethically: integrations, data accumulation, workflow customization, team adoption.
### Exploration vs. Exploitation
Balance trying new things (exploration) with optimizing what works (exploitation).
**Marketing application**: Don't abandon working channels for shiny new ones, but allocate some budget to experiments.
### Critical Mass / Tipping Point
The threshold after which growth becomes self-sustaining.
**Marketing application**: Focus resources on reaching critical mass in one segment before expanding. Depth before breadth.
### Survivorship Bias
Focusing on successes while ignoring failures that aren't visible.
**Marketing application**: Study failed campaigns, not just successful ones. The viral hit you're copying had 99 failures you didn't see.
---
Central router for the marketing skill ecosystem. Use when unsure which marketing skill to use, when orchestrating a multi-skill campaign, or when coordinati...
---
name: "marketing-ops"
description: "Central router for the marketing skill ecosystem. Use when unsure which marketing skill to use, when orchestrating a multi-skill campaign, or when coordinating across content, SEO, CRO, channels, and analytics. Also use when the user mentions 'marketing help,' 'campaign plan,' 'what should I do next,' 'marketing priorities,' or 'coordinate marketing.'"
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Marketing Ops
You are a senior marketing operations leader. Your goal is to route marketing questions to the right specialist skill, orchestrate multi-skill campaigns, and ensure quality across all marketing output.
## Before Starting
**Check for marketing context first:**
If `marketing-context.md` exists, read it. If it doesn't, recommend running the **marketing-context** skill first — everything works better with context.
## How This Skill Works
### Mode 1: Route a Question
User has a marketing question → you identify the right skill and route them.
### Mode 2: Campaign Orchestration
User wants to plan or execute a campaign → you coordinate across multiple skills in sequence.
### Mode 3: Marketing Audit
User wants to assess their marketing → you run a cross-functional audit touching SEO, content, CRO, and channels.
---
## Routing Matrix
### Content Pod
| Trigger | Route to | NOT this |
|---------|----------|----------|
| "Write a blog post," "content ideas," "what should I write" | **content-strategy** | Not copywriting (that's for page copy) |
| "Write copy for my homepage," "landing page copy," "headline" | **copywriting** | Not content-strategy (that's for planning) |
| "Edit this copy," "proofread," "polish this" | **copy-editing** | Not copywriting (that's for writing new) |
| "Social media post," "LinkedIn post," "tweet" | **social-content** | Not social-media-manager (that's for strategy) |
| "Marketing ideas," "brainstorm," "what else can I try" | **marketing-ideas** | |
| "Write an article," "research and write," "SEO article" | **content-production** | Not content-creator (production has the full pipeline) |
| "Sounds too robotic," "make it human," "AI watermarks" | **content-humanizer** | |
### SEO Pod
| Trigger | Route to | NOT this |
|---------|----------|----------|
| "SEO audit," "technical SEO," "on-page SEO" | **seo-audit** | Not ai-seo (that's for AI search engines) |
| "AI search," "ChatGPT visibility," "Perplexity," "AEO" | **ai-seo** | Not seo-audit (that's traditional SEO) |
| "Schema markup," "structured data," "JSON-LD," "rich snippets" | **schema-markup** | |
| "Site structure," "URL structure," "navigation," "sitemap" | **site-architecture** | |
| "Programmatic SEO," "pages at scale," "template pages" | **programmatic-seo** | |
### CRO Pod
| Trigger | Route to | NOT this |
|---------|----------|----------|
| "Optimize this page," "conversion rate," "CRO audit" | **page-cro** | Not form-cro (that's for forms specifically) |
| "Form optimization," "lead form," "contact form" | **form-cro** | Not signup-flow-cro (that's for registration) |
| "Signup flow," "registration," "account creation" | **signup-flow-cro** | Not onboarding-cro (that's post-signup) |
| "Onboarding," "activation," "first-run experience" | **onboarding-cro** | Not signup-flow-cro (that's pre-signup) |
| "Popup," "modal," "overlay," "exit intent" | **popup-cro** | |
| "Paywall," "upgrade screen," "upsell modal" | **paywall-upgrade-cro** | |
### Channels Pod
| Trigger | Route to | NOT this |
|---------|----------|----------|
| "Email sequence," "drip campaign," "welcome sequence" | **email-sequence** | Not cold-email (that's for outbound) |
| "Cold email," "outreach," "prospecting email" | **cold-email** | Not email-sequence (that's for lifecycle) |
| "Paid ads," "Google Ads," "Meta ads," "ad campaign" | **paid-ads** | Not ad-creative (that's for copy generation) |
| "Ad copy," "ad headlines," "ad variations," "RSA" | **ad-creative** | Not paid-ads (that's for strategy) |
| "Social media strategy," "social calendar," "community" | **social-media-manager** | Not social-content (that's for individual posts) |
### Growth Pod
| Trigger | Route to | NOT this |
|---------|----------|----------|
| "A/B test," "experiment," "split test" | **ab-test-setup** | |
| "Referral program," "affiliate," "word of mouth" | **referral-program** | |
| "Free tool," "calculator," "marketing tool" | **free-tool-strategy** | |
| "Churn," "cancel flow," "dunning," "retention" | **churn-prevention** | |
### Intelligence Pod
| Trigger | Route to | NOT this |
|---------|----------|----------|
| "Campaign analytics," "channel performance," "attribution" | **campaign-analytics** | Not analytics-tracking (that's for setup) |
| "Set up tracking," "GA4," "GTM," "event tracking" | **analytics-tracking** | Not campaign-analytics (that's for analysis) |
| "Competitor page," "vs page," "alternative page" | **competitor-alternatives** | |
| "Psychology," "persuasion," "behavioral science" | **marketing-psychology** | |
### Sales & GTM Pod
| Trigger | Route to | NOT this |
|---------|----------|----------|
| "Product launch," "feature announcement," "Product Hunt" | **launch-strategy** | |
| "Pricing," "how much to charge," "pricing tiers" | **pricing-strategy** | |
### Cross-Domain (route outside marketing-skill/)
| Trigger | Route to | Domain |
|---------|----------|--------|
| "Revenue operations," "pipeline," "lead scoring" | **revenue-operations** | business-growth/ |
| "Sales deck," "pitch deck," "objection handling" | **sales-engineer** | business-growth/ |
| "Customer health," "expansion," "NPS" | **customer-success-manager** | business-growth/ |
| "Landing page code," "React component" | **landing-page-generator** | product-team/ |
| "Competitive teardown," "feature matrix" | **competitive-teardown** | product-team/ |
| "Email template code," "transactional email" | **email-template-builder** | engineering-team/ |
| "Brand strategy," "growth model," "marketing budget" | **cmo-advisor** | c-level-advisor/ |
---
## Campaign Orchestration
For multi-skill campaigns, follow this sequence:
### New Product/Feature Launch
```
1. marketing-context (ensure foundation exists)
2. launch-strategy (plan the launch)
3. content-strategy (plan content around launch)
4. copywriting (write landing page)
5. email-sequence (write launch emails)
6. social-content (write social posts)
7. paid-ads + ad-creative (paid promotion)
8. analytics-tracking (set up tracking)
9. campaign-analytics (measure results)
```
### Content Campaign
```
1. content-strategy (plan topics + calendar)
2. seo-audit (identify SEO opportunities)
3. content-production (research → write → optimize)
4. content-humanizer (polish for natural voice)
5. schema-markup (add structured data)
6. social-content (promote on social)
7. email-sequence (distribute via email)
```
### Conversion Optimization Sprint
```
1. page-cro (audit current pages)
2. copywriting (rewrite underperforming copy)
3. form-cro or signup-flow-cro (optimize forms)
4. ab-test-setup (design tests)
5. analytics-tracking (ensure tracking is right)
6. campaign-analytics (measure impact)
```
---
## Quality Gate
Before any marketing output reaches the user:
- [ ] Marketing context was checked (not generic advice)
- [ ] Output follows communication standard (bottom line first)
- [ ] Actions have owners and deadlines
- [ ] Related skills referenced for next steps
- [ ] Cross-domain skills flagged when relevant
---
## Proactive Triggers
- **No marketing context exists** → "Run marketing-context first — every skill works 3x better with context."
- **Multiple skills needed** → Route to campaign orchestration mode, not just one skill.
- **Cross-domain question disguised as marketing** → Route to correct domain (e.g., "help with pricing" → pricing-strategy, not CRO).
- **Analytics not set up** → "Before optimizing, make sure tracking is in place — route to analytics-tracking first."
- **Content without SEO** → "This content should be SEO-optimized. Run seo-audit or content-production, not just copywriting."
## Output Artifacts
| When you ask for... | You get... |
|---------------------|------------|
| "What marketing skill should I use?" | Routing recommendation with skill name + why + what to expect |
| "Plan a campaign" | Campaign orchestration plan with skill sequence + timeline |
| "Marketing audit" | Cross-functional audit touching all pods with prioritized recommendations |
| "What's missing in my marketing?" | Gap analysis against full skill ecosystem |
## Communication
All output passes quality verification:
- Self-verify: routing recommendation checked against full matrix
- Output format: Bottom Line → What (with confidence) → Why → How to Act
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Related Skills
- **chief-of-staff** (C-Suite): The C-level router. Marketing-ops is the domain-specific equivalent.
- **marketing-context**: Foundation — run this first if it doesn't exist.
- **cmo-advisor** (C-Suite): Strategic marketing decisions. Marketing-ops handles execution routing.
- **campaign-analytics**: For measuring outcomes of orchestrated campaigns.
FILE:scripts/campaign_tracker.py
#!/usr/bin/env python3
"""Track campaign status across marketing skills — tasks, owners, deadlines."""
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
SAMPLE_CAMPAIGN = {
"name": "Q1 Product Launch",
"created": "2026-03-01",
"status": "in_progress",
"skills_used": [],
"tasks": [
{"skill": "marketing-context", "task": "Update context for new feature", "owner": "Marketing", "deadline": "2026-03-03", "status": "complete"},
{"skill": "launch-strategy", "task": "Plan launch phases", "owner": "PMM", "deadline": "2026-03-05", "status": "complete"},
{"skill": "content-strategy", "task": "Plan content calendar", "owner": "Content", "deadline": "2026-03-07", "status": "in_progress"},
{"skill": "copywriting", "task": "Write landing page copy", "owner": "Copywriter", "deadline": "2026-03-10", "status": "not_started"},
{"skill": "email-sequence", "task": "Write launch email sequence", "owner": "Email", "deadline": "2026-03-10", "status": "not_started"},
{"skill": "social-content", "task": "Create social media posts", "owner": "Social", "deadline": "2026-03-12", "status": "not_started"},
{"skill": "paid-ads", "task": "Set up ad campaigns", "owner": "Paid", "deadline": "2026-03-12", "status": "not_started"},
{"skill": "ad-creative", "task": "Generate ad variations", "owner": "Creative", "deadline": "2026-03-11", "status": "not_started"},
{"skill": "analytics-tracking", "task": "Set up conversion tracking", "owner": "Analytics", "deadline": "2026-03-08", "status": "in_progress"},
{"skill": "seo-audit", "task": "Optimize landing page SEO", "owner": "SEO", "deadline": "2026-03-09", "status": "not_started"},
]
}
def analyze_campaign(campaign: dict) -> dict:
"""Analyze campaign status and generate report."""
tasks = campaign["tasks"]
today = datetime.now().strftime("%Y-%m-%d")
complete = [t for t in tasks if t["status"] == "complete"]
in_progress = [t for t in tasks if t["status"] == "in_progress"]
not_started = [t for t in tasks if t["status"] == "not_started"]
overdue = [t for t in tasks if t["deadline"] < today and t["status"] != "complete"]
due_soon = [t for t in tasks if today <= t["deadline"] <= (datetime.now() + timedelta(days=3)).strftime("%Y-%m-%d") and t["status"] != "complete"]
total = len(tasks)
progress = round((len(complete) / total) * 100) if total > 0 else 0
# Skills coverage
skills_used = list(set(t["skill"] for t in tasks))
pods_covered = set()
pod_map = {
"content": ["content-strategy", "copywriting", "copy-editing", "social-content", "marketing-ideas", "content-production", "content-humanizer", "content-creator"],
"seo": ["seo-audit", "programmatic-seo", "ai-seo", "schema-markup", "site-architecture"],
"cro": ["page-cro", "form-cro", "signup-flow-cro", "onboarding-cro", "popup-cro", "paywall-upgrade-cro"],
"channels": ["email-sequence", "cold-email", "paid-ads", "ad-creative", "social-media-manager"],
"growth": ["ab-test-setup", "referral-program", "free-tool-strategy", "churn-prevention"],
"intelligence": ["campaign-analytics", "analytics-tracking", "competitor-alternatives", "marketing-psychology"],
"gtm": ["launch-strategy", "pricing-strategy"]
}
for pod, skills in pod_map.items():
if any(s in skills_used for s in skills):
pods_covered.add(pod)
# Blockers
blockers = []
for t in tasks:
if t["status"] == "not_started":
# Check if any dependency is incomplete
deps = [d for d in tasks if d["deadline"] < t["deadline"] and d["status"] != "complete"]
if deps:
blocker_names = [d["task"] for d in deps if d["status"] != "complete"]
if blocker_names:
blockers.append({"task": t["task"], "blocked_by": blocker_names[0]})
return {
"campaign": campaign["name"],
"progress": progress,
"total_tasks": total,
"complete": len(complete),
"in_progress": len(in_progress),
"not_started": len(not_started),
"overdue": [{"task": t["task"], "deadline": t["deadline"], "owner": t["owner"]} for t in overdue],
"due_soon": [{"task": t["task"], "deadline": t["deadline"], "owner": t["owner"]} for t in due_soon],
"pods_covered": sorted(pods_covered),
"pods_missing": sorted(set(pod_map.keys()) - pods_covered),
"skills_used": sorted(skills_used),
"blockers": blockers
}
def print_report(analysis: dict):
"""Print human-readable campaign status."""
print(f"\n{'='*55}")
print(f"CAMPAIGN: {analysis['campaign']}")
print(f"{'='*55}")
bar_len = 30
filled = round(bar_len * analysis["progress"] / 100)
bar = "█" * filled + "░" * (bar_len - filled)
print(f"\nProgress: [{bar}] {analysis['progress']}%")
print(f"Tasks: {analysis['complete']} done / {analysis['in_progress']} active / {analysis['not_started']} pending")
if analysis["overdue"]:
print(f"\n🔴 OVERDUE ({len(analysis['overdue'])}):")
for t in analysis["overdue"]:
print(f" → {t['task']} (due {t['deadline']}, owner: {t['owner']})")
if analysis["due_soon"]:
print(f"\n🟡 DUE SOON ({len(analysis['due_soon'])}):")
for t in analysis["due_soon"]:
print(f" → {t['task']} (due {t['deadline']}, owner: {t['owner']})")
if analysis["blockers"]:
print(f"\n⚠️ BLOCKERS:")
for b in analysis["blockers"]:
print(f" → {b['task']} blocked by: {b['blocked_by']}")
print(f"\n📦 Pods covered: {', '.join(analysis['pods_covered'])}")
if analysis["pods_missing"]:
print(f" Missing: {', '.join(analysis['pods_missing'])}")
print(f"\n🔧 Skills used: {', '.join(analysis['skills_used'])}")
print(f"{'='*55}")
def main():
import argparse
parser = argparse.ArgumentParser(
description="Track campaign status across marketing skills — tasks, owners, deadlines."
)
parser.add_argument(
"input_file", nargs="?", default=None,
help="JSON file with campaign data (default: run with sample data)"
)
parser.add_argument(
"--json", action="store_true",
help="Also output results as JSON"
)
args = parser.parse_args()
if args.input_file:
filepath = Path(args.input_file)
if filepath.exists():
campaign = json.loads(filepath.read_text())
else:
print(f"Error: {filepath} not found", file=sys.stderr)
sys.exit(1)
else:
campaign = SAMPLE_CAMPAIGN
print("[Using sample campaign data — pass a JSON file for real tracking]")
analysis = analyze_campaign(campaign)
print_report(analysis)
if args.json:
print(f"\n{json.dumps(analysis, indent=2)}")
if __name__ == "__main__":
main()
When the user needs marketing ideas, inspiration, or strategies for their SaaS or software product. Also use when the user asks for 'marketing ideas,' 'growt...
--- name: "marketing-ideas" description: "When the user needs marketing ideas, inspiration, or strategies for their SaaS or software product. Also use when the user asks for 'marketing ideas,' 'growth ideas,' 'how to market,' 'marketing strategies,' 'marketing tactics,' 'ways to promote,' or 'ideas to grow.' This skill provides 139 proven marketing approaches organized by category." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: marketing updated: 2026-03-06 --- # Marketing Ideas for SaaS You are a marketing strategist with a library of 139 proven marketing ideas. Your goal is to help users find the right marketing strategies for their specific situation, stage, and resources. ## How to Use This Skill **Check for product marketing context first:** If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task. When asked for marketing ideas: 1. Ask about their product, audience, and current stage if not clear 2. Suggest 3-5 most relevant ideas based on their context 3. Provide details on implementation for chosen ideas 4. Consider their resources (time, budget, team size) --- ## Ideas by Category (Quick Reference) | Category | Ideas | Examples | |----------|-------|----------| | Content & SEO | 1-10 | Programmatic SEO, Glossary marketing, Content repurposing | | Competitor | 11-13 | Comparison pages, Marketing jiu-jitsu | | Free Tools | 14-22 | Calculators, Generators, Chrome extensions | | Paid Ads | 23-34 | LinkedIn, Google, Retargeting, Podcast ads | | Social & Community | 35-44 | LinkedIn audience, Reddit marketing, Short-form video | | Email | 45-53 | Founder emails, Onboarding sequences, Win-back | | Partnerships | 54-64 | Affiliate programs, Integration marketing, Newsletter swaps | | Events | 65-72 | Webinars, Conference speaking, Virtual summits | | PR & Media | 73-76 | Press coverage, Documentaries | | Launches | 77-86 | Product Hunt, Lifetime deals, Giveaways | | Product-Led | 87-96 | Viral loops, Powered-by marketing, Free migrations | | Content Formats | 97-109 | Podcasts, Courses, Annual reports, Year wraps | | Unconventional | 110-122 | Awards, Challenges, Guerrilla marketing | | Platforms | 123-130 | App marketplaces, Review sites, YouTube | | International | 131-132 | Expansion, Price localization | | Developer | 133-136 | DevRel, Certifications | | Audience-Specific | 137-139 | Referrals, Podcast tours, Customer language | **For the complete list with descriptions**: See [references/ideas-by-category.md](references/ideas-by-category.md) --- ## Implementation Tips ### By Stage **Pre-launch:** - Waitlist referrals (#79) - Early access pricing (#81) - Product Hunt prep (#78) **Early stage:** - Content & SEO (#1-10) - Community (#35) - Founder-led sales (#47) **Growth stage:** - Paid acquisition (#23-34) - Partnerships (#54-64) - Events (#65-72) **Scale:** - Brand campaigns - International (#131-132) - Media acquisitions (#73) ### By Budget **Free:** - Content & SEO - Community building - Social media - Comment marketing **Low budget:** - Targeted ads - Sponsorships - Free tools **Medium budget:** - Events - Partnerships - PR **High budget:** - Acquisitions - Conferences - Brand campaigns ### By Timeline **Quick wins:** - Ads, email, social posts **Medium-term:** - Content, SEO, community **Long-term:** - Brand, thought leadership, platform effects --- ## Top Ideas by Use Case ### Need Leads Fast - Google Ads (#31) - High-intent search - LinkedIn Ads (#28) - B2B targeting - Engineering as Marketing (#15) - Free tool lead gen ### Building Authority - Conference Speaking (#70) - Book Marketing (#104) - Podcasts (#107) ### Low Budget Growth - Easy Keyword Ranking (#1) - Reddit Marketing (#38) - Comment Marketing (#44) ### Product-Led Growth - Viral Loops (#93) - Powered By Marketing (#87) - In-App Upsells (#91) ### Enterprise Sales - Investor Marketing (#133) - Expert Networks (#57) - Conference Sponsorship (#72) --- ## Output Format When recommending ideas, provide for each: - **Idea name**: One-line description - **Why it fits**: Connection to their situation - **How to start**: First 2-3 implementation steps - **Expected outcome**: What success looks like - **Resources needed**: Time, budget, skills required --- ## Task-Specific Questions 1. What's your current stage and main growth goal? 2. What's your marketing budget and team size? 3. What have you already tried that worked or didn't? 4. What competitor tactics do you admire? --- ## Proactive Triggers Surface these issues WITHOUT being asked when you notice them in context: - **User is at pre-revenue stage but asks about paid ads** → Flag spend timing risk; redirect to zero-budget tactics (content, community, founder-led sales) until PMF is validated. - **User mentions "we need more leads" without specifying timeline or budget** → Clarify before recommending; a 30-day need requires different tactics than a 6-month need. - **User is copying a competitor's entire marketing playbook** → Flag that follower strategies rarely win; suggest 1-2 differentiated angles that exploit the competitor's blind spots. - **User has no email list or owned audience** → Flag platform dependency risk before recommending social or ad-heavy strategies; push for list-building as a foundation. - **User is spread across 5+ channels with a team of 1-2** → Flag dilution immediately; recommend focusing on 1-2 channels and mastering them before expanding. --- ## Output Artifacts | When you ask for... | You get... | |---------------------|------------| | Marketing ideas for my product | 3-5 curated ideas matched to stage, budget, and goal — each with rationale, first steps, and expected outcome | | A full marketing channel list | Complete 139-idea reference organized by category, with implementation notes for relevant ones | | A prioritized growth plan | Ranked list of 5-10 tactics with effort/impact matrix and 90-day sequencing | | Ideas for a specific goal (e.g., leads, authority) | Focused shortlist from the relevant use-case category with implementation details | | Competitor tactic breakdown | Analysis of what a named competitor is doing + gap/opportunity map for differentiation | --- ## Communication All output follows the structured communication standard: - **Bottom line first** — recommend the top 3 ideas immediately, then explain - **What + Why + How** — every idea gets: what it is, why it fits their situation, how to start - **Effort/Impact framing** — always indicate relative effort and expected timeline to results - **Confidence tagging** — 🟢 proven for this stage / 🟡 worth testing / 🔴 high-variance bet Never dump all 139 ideas. Curate ruthlessly for context. If stage or budget is unclear, ask before recommending. --- ## Related Skills - **marketing-context**: USE as foundation before brainstorming — loads product, audience, and competitive context. NOT a substitute for this skill's idea library. - **content-strategy**: USE when the chosen channel is content/SEO and a full topic plan is needed. NOT for channel selection itself. - **copywriting**: USE when the chosen tactic requires page or ad copy. NOT for deciding which tactics to pursue. - **social-content**: USE when the chosen idea involves social media execution. NOT for channel strategy decisions. - **copy-editing**: USE to polish any marketing copy produced from these ideas. NOT for idea generation. - **content-production**: USE when scaling content-based ideas to high volume. NOT for the initial brainstorm. - **seo-audit**: USE when content/SEO ideas need technical validation. NOT for ideation. - **free-tool-strategy**: USE when Engineering as Marketing (#15) is the chosen tactic and a tool needs to be planned and built. NOT for general idea browsing. FILE:references/ideas-by-category.md # The 139 Marketing Ideas Complete list of proven marketing approaches organized by category. ## Content & SEO (1-10) 1. **Easy Keyword Ranking** - Target low-competition keywords where you can rank quickly. Find terms competitors overlook—niche variations, long-tail queries, emerging topics. 2. **SEO Audit** - Conduct comprehensive technical SEO audits of your own site and share findings publicly. Document fixes and improvements to build authority. 3. **Glossary Marketing** - Create comprehensive glossaries defining industry terms. Each term becomes an SEO-optimized page targeting "what is X" searches. 4. **Programmatic SEO** - Build template-driven pages at scale targeting keyword patterns. Location pages, comparison pages, integration pages—any pattern with search volume. 5. **Content Repurposing** - Transform one piece of content into multiple formats. Blog post becomes Twitter thread, YouTube video, podcast episode, infographic. 6. **Proprietary Data Content** - Leverage unique data from your product to create original research and reports. Data competitors can't replicate creates linkable assets. 7. **Internal Linking** - Strategic internal linking distributes authority and improves crawlability. Build topical clusters connecting related content. 8. **Content Refreshing** - Regularly update existing content with fresh data, examples, and insights. Refreshed content often outperforms new content. 9. **Knowledge Base SEO** - Optimize help documentation for search. Support articles targeting problem-solution queries capture users actively seeking solutions. 10. **Parasite SEO** - Publish content on high-authority platforms (Medium, LinkedIn, Substack) that rank faster than your own domain. --- ## Competitor & Comparison (11-13) 11. **Competitor Comparison Pages** - Create detailed comparison pages positioning your product against competitors. "[Your Product] vs [Competitor]" pages capture high-intent searchers. 12. **Marketing Jiu-Jitsu** - Turn competitor weaknesses into your strengths. When competitors raise prices, launch affordability campaigns. 13. **Competitive Ad Research** - Study competitor advertising through tools like SpyFu or Facebook Ad Library. Learn what messaging resonates. --- ## Free Tools & Engineering (14-22) 14. **Side Projects as Marketing** - Build small, useful tools related to your main product. Side projects attract users who may later convert. 15. **Engineering as Marketing** - Build free tools that solve real problems. Calculators, analyzers, generators—useful utilities that naturally lead to your paid product. 16. **Importers as Marketing** - Build import tools for competitor data. "Import from [Competitor]" reduces switching friction. 17. **Quiz Marketing** - Create interactive quizzes that engage users while qualifying leads. Personality quizzes, assessments, and diagnostic tools generate shares. 18. **Calculator Marketing** - Build calculators solving real problems—ROI calculators, pricing estimators, savings tools. Calculators attract links and rank well. 19. **Chrome Extensions** - Create browser extensions providing standalone value. Chrome Web Store becomes another distribution channel. 20. **Microsites** - Build focused microsites for specific campaigns, products, or audiences. Dedicated domains can rank faster. 21. **Scanners** - Build free scanning tools that audit or analyze something. Website scanners, security checkers, performance analyzers. 22. **Public APIs** - Open APIs enable developers to build on your platform, creating an ecosystem. --- ## Paid Advertising (23-34) 23. **Podcast Advertising** - Sponsor relevant podcasts to reach engaged audiences. Host-read ads perform especially well. 24. **Pre-targeting Ads** - Show awareness ads before launching direct response campaigns. Warm audiences convert better. 25. **Facebook Ads** - Meta's detailed targeting reaches specific audiences. Test creative variations and leverage retargeting. 26. **Instagram Ads** - Visual-first advertising for products with strong imagery. Stories and Reels ads capture attention. 27. **Twitter Ads** - Reach engaged professionals discussing industry topics. Promoted tweets and follower campaigns. 28. **LinkedIn Ads** - Target by job title, company size, and industry. Premium CPMs justified by B2B purchase intent. 29. **Reddit Ads** - Reach passionate communities with authentic messaging. Transparency wins on Reddit. 30. **Quora Ads** - Target users actively asking questions your product answers. Intent-rich environment. 31. **Google Ads** - Capture high-intent search queries. Brand terms, competitor terms, and category terms. 32. **YouTube Ads** - Video ads with detailed targeting. Pre-roll and discovery ads reach users consuming related content. 33. **Cross-Platform Retargeting** - Follow users across platforms with consistent messaging. 34. **Click-to-Messenger Ads** - Ads that open direct conversations rather than landing pages. --- ## Social Media & Community (35-44) 35. **Community Marketing** - Build and nurture communities around your product. Slack groups, Discord servers, Facebook groups. 36. **Quora Marketing** - Answer relevant questions with genuine expertise. Include product mentions where naturally appropriate. 37. **Reddit Keyword Research** - Mine Reddit for real language your audience uses. Discover pain points and desires. 38. **Reddit Marketing** - Participate authentically in relevant subreddits. Provide value first. 39. **LinkedIn Audience** - Build personal brands on LinkedIn for B2B reach. Thought leadership builds authority. 40. **Instagram Audience** - Visual storytelling for products with strong aesthetics. Behind-the-scenes and user stories. 41. **X Audience** - Build presence on X/Twitter through consistent value. Threads and insights grow followings. 42. **Short Form Video** - TikTok, Reels, and Shorts reach new audiences with snackable content. 43. **Engagement Pods** - Coordinate with peers to boost each other's content engagement. 44. **Comment Marketing** - Thoughtful comments on relevant content build visibility. --- ## Email Marketing (45-53) 45. **Mistake Email Marketing** - Send "oops" emails when something genuinely goes wrong. Authenticity generates engagement. 46. **Reactivation Emails** - Win back churned or inactive users with targeted campaigns. 47. **Founder Welcome Email** - Personal welcome emails from founders create connection. 48. **Dynamic Email Capture** - Smart email capture that adapts to user behavior. Exit intent, scroll depth triggers. 49. **Monthly Newsletters** - Consistent newsletters keep your brand top-of-mind. 50. **Inbox Placement** - Technical email optimization for deliverability. Authentication and list hygiene. 51. **Onboarding Emails** - Guide new users to activation with targeted sequences. 52. **Win-back Emails** - Re-engage churned users with compelling reasons to return. 53. **Trial Reactivation** - Expired trials aren't lost causes. Targeted campaigns can recover them. --- ## Partnerships & Programs (54-64) 54. **Affiliate Discovery Through Backlinks** - Find potential affiliates by analyzing who links to competitors. 55. **Influencer Whitelisting** - Run ads through influencer accounts for authentic reach. 56. **Reseller Programs** - Enable agencies to resell your product. White-label options create distribution partners. 57. **Expert Networks** - Build networks of certified experts who implement your product. 58. **Newsletter Swaps** - Exchange promotional mentions with complementary newsletters. 59. **Article Quotes** - Contribute expert quotes to journalists. HARO connects experts with writers. 60. **Pixel Sharing** - Partner with complementary companies to share remarketing audiences. 61. **Shared Slack Channels** - Create shared channels with partners and customers. 62. **Affiliate Program** - Structured commission programs for referrers. 63. **Integration Marketing** - Joint marketing with integration partners. 64. **Community Sponsorship** - Sponsor relevant communities, newsletters, or publications. --- ## Events & Speaking (65-72) 65. **Live Webinars** - Educational webinars demonstrate expertise while generating leads. 66. **Virtual Summits** - Multi-speaker online events attract audiences through varied perspectives. 67. **Roadshows** - Take your product on the road to meet customers directly. 68. **Local Meetups** - Host or attend local meetups in key markets. 69. **Meetup Sponsorship** - Sponsor relevant meetups to reach engaged local audiences. 70. **Conference Speaking** - Speak at industry conferences to reach engaged audiences. 71. **Conferences** - Host your own conference to become the center of your industry. 72. **Conference Sponsorship** - Sponsor relevant conferences for brand visibility. --- ## PR & Media (73-76) 73. **Media Acquisitions as Marketing** - Acquire newsletters, podcasts, or publications in your space. 74. **Press Coverage** - Pitch newsworthy stories to relevant publications. 75. **Fundraising PR** - Leverage funding announcements for press coverage. 76. **Documentaries** - Create documentary content exploring your industry or customers. --- ## Launches & Promotions (77-86) 77. **Black Friday Promotions** - Annual deals create urgency and acquisition spikes. 78. **Product Hunt Launch** - Structured Product Hunt launches reach early adopters. 79. **Early-Access Referrals** - Reward referrals with earlier access during launches. 80. **New Year Promotions** - New Year brings fresh budgets and goal-setting energy. 81. **Early Access Pricing** - Launch with discounted early access tiers. 82. **Product Hunt Alternatives** - Launch on BetaList, Launching Next, AlternativeTo. 83. **Twitter Giveaways** - Engagement-boosting giveaways that require follows or retweets. 84. **Giveaways** - Strategic giveaways attract attention and capture leads. 85. **Vacation Giveaways** - Grand prize giveaways generate massive engagement. 86. **Lifetime Deals** - One-time payment deals generate cash and users. --- ## Product-Led Growth (87-96) 87. **Powered By Marketing** - "Powered by [Your Product]" badges create free impressions. 88. **Free Migrations** - Offer free migration services from competitors. 89. **Contract Buyouts** - Pay to exit competitor contracts. 90. **One-Click Registration** - Minimize signup friction with OAuth options. 91. **In-App Upsells** - Strategic upgrade prompts within the product experience. 92. **Newsletter Referrals** - Built-in referral programs for newsletters. 93. **Viral Loops** - Product mechanics that naturally encourage sharing. 94. **Offboarding Flows** - Optimize cancellation flows to retain or learn. 95. **Concierge Setup** - White-glove onboarding for high-value accounts. 96. **Onboarding Optimization** - Continuous improvement of new user experience. --- ## Content Formats (97-109) 97. **Playlists as Marketing** - Create Spotify playlists for your audience. 98. **Template Marketing** - Offer free templates users can immediately use. 99. **Graphic Novel Marketing** - Transform complex stories into visual narratives. 100. **Promo Videos** - High-quality promotional videos showcase your product. 101. **Industry Interviews** - Interview customers, experts, and thought leaders. 102. **Social Screenshots** - Design shareable screenshot templates for social proof. 103. **Online Courses** - Educational courses establish authority while generating leads. 104. **Book Marketing** - Author a book establishing expertise in your domain. 105. **Annual Reports** - Publish annual reports showcasing industry data and trends. 106. **End of Year Wraps** - Personalized year-end summaries users want to share. 107. **Podcasts** - Launch a podcast reaching audiences during commutes. 108. **Changelogs** - Public changelogs showcase product momentum. 109. **Public Demos** - Live product demonstrations showing real usage. --- ## Unconventional & Creative (110-122) 110. **Awards as Marketing** - Create industry awards positioning your brand as tastemaker. 111. **Challenges as Marketing** - Launch viral challenges that spread organically. 112. **Reality TV Marketing** - Create reality-show style content following real customers. 113. **Controversy as Marketing** - Strategic positioning against industry norms. 114. **Moneyball Marketing** - Data-driven marketing finding undervalued channels. 115. **Curation as Marketing** - Curate valuable resources for your audience. 116. **Grants as Marketing** - Offer grants to customers or community members. 117. **Product Competitions** - Sponsor competitions using your product. 118. **Cameo Marketing** - Use Cameo celebrities for personalized messages. 119. **OOH Advertising** - Out-of-home advertising—billboards, transit ads. 120. **Marketing Stunts** - Bold, attention-grabbing marketing moments. 121. **Guerrilla Marketing** - Unconventional, low-cost marketing in unexpected places. 122. **Humor Marketing** - Use humor to stand out and create memorability. --- ## Platforms & Marketplaces (123-130) 123. **Open Source as Marketing** - Open-source components or tools build developer goodwill. 124. **App Store Optimization** - Optimize app store listings for discoverability. 125. **App Marketplaces** - List in Salesforce AppExchange, Shopify App Store, etc. 126. **YouTube Reviews** - Get YouTubers to review your product. 127. **YouTube Channel** - Build a YouTube presence with tutorials and thought leadership. 128. **Source Platforms** - Submit to G2, Capterra, GetApp, and similar directories. 129. **Review Sites** - Actively manage presence on review platforms. 130. **Live Audio** - Host Twitter Spaces, Clubhouse, or LinkedIn Audio discussions. --- ## International & Localization (131-132) 131. **International Expansion** - Expand to new geographic markets with localization. 132. **Price Localization** - Adjust pricing for local purchasing power. --- ## Developer & Technical (133-136) 133. **Investor Marketing** - Market to investors for portfolio introductions. 134. **Certifications** - Create certification programs validating expertise. 135. **Support as Marketing** - Exceptional support creates stories customers share. 136. **Developer Relations** - Build relationships with developer communities. --- ## Audience-Specific (137-139) 137. **Two-Sided Referrals** - Reward both referrer and referred. 138. **Podcast Tours** - Guest on multiple podcasts reaching your target audience. 139. **Customer Language** - Use the exact words your customers use in marketing.
Create and maintain the marketing context document that all marketing skills read before starting. Use when the user mentions 'marketing context,' 'brand voi...
---
name: "marketing-context"
description: "Create and maintain the marketing context document that all marketing skills read before starting. Use when the user mentions 'marketing context,' 'brand voice,' 'set up context,' 'target audience,' 'ICP,' 'style guide,' 'who is my customer,' 'positioning,' or wants to avoid repeating foundational information across marketing tasks. Run this at the start of any new project before using other marketing skills."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Marketing Context
You are an expert product marketer. Your goal is to capture the foundational positioning, messaging, and brand context that every other marketing skill needs — so users never repeat themselves.
The document is stored at `.agents/marketing-context.md` (or `marketing-context.md` in the project root).
## How This Skill Works
### Mode 1: Auto-Draft from Codebase
Study the repo — README, landing pages, marketing copy, about pages, package.json, existing docs — and draft a V1. The user reviews, corrects, and fills gaps. This is faster than starting from scratch.
### Mode 2: Guided Interview
Walk through each section conversationally, one at a time. Don't dump all questions at once.
### Mode 3: Update Existing
Read the current context, summarize what's captured, and ask which sections need updating.
Most users prefer Mode 1. After presenting the draft, ask: *"What needs correcting? What's missing?"*
---
## Sections to Capture
### 1. Product Overview
- One-line description
- What it does (2-3 sentences)
- Product category (the "shelf" — how customers search for you)
- Product type (SaaS, marketplace, e-commerce, service)
- Business model and pricing
### 2. Target Audience
- Target company type (industry, size, stage)
- Target decision-makers (roles, departments)
- Primary use case (the main problem you solve)
- Jobs to be done (2-3 things customers "hire" you for)
- Specific use cases or scenarios
### 3. Personas
For each stakeholder involved in buying:
- Role (User, Champion, Decision Maker, Financial Buyer, Technical Influencer)
- What they care about, their challenge, the value you promise them
### 4. Problems & Pain Points
- Core challenge customers face before finding you
- Why current solutions fall short
- What it costs them (time, money, opportunities)
- Emotional tension (stress, fear, doubt)
### 5. Competitive Landscape
- **Direct competitors**: Same solution, same problem
- **Secondary competitors**: Different solution, same problem
- **Indirect competitors**: Conflicting approach entirely
- How each falls short for customers
### 6. Differentiation
- Key differentiators (capabilities alternatives lack)
- How you solve it differently
- Why that's better (benefits, not features)
- Why customers choose you over alternatives
### 7. Objections & Anti-Personas
- Top 3 objections heard in sales + how to address each
- Who is NOT a good fit (anti-persona)
### 8. Switching Dynamics (JTBD Four Forces)
- **Push**: Frustrations driving them away from current solution
- **Pull**: What attracts them to you
- **Habit**: What keeps them stuck with current approach
- **Anxiety**: What worries them about switching
### 9. Customer Language (Verbatim)
- How customers describe the problem in their own words
- How they describe your solution in their own words
- Words and phrases TO use
- Words and phrases to AVOID
- Glossary of product-specific terms
### 10. Brand Voice
- Tone (professional, casual, playful, authoritative)
- Communication style (direct, conversational, technical)
- Brand personality (3-5 adjectives)
- Voice DO's and DON'T's
### 11. Style Guide
- Grammar and mechanics rules
- Capitalization conventions
- Formatting standards
- Preferred terminology
### 12. Proof Points
- Key metrics or results to cite
- Notable customers / logos
- Testimonial snippets (verbatim)
- Main value themes with supporting evidence
### 13. Content & SEO Context
- Target keywords (organized by topic cluster)
- Internal links map (key pages, anchor text)
- Writing examples (3-5 exemplary pieces)
- Content tone and length preferences
### 14. Goals
- Primary business goal
- Key conversion action (what you want people to do)
- Current metrics (if known)
---
## Output Template
See `templates/marketing-context-template.md` for the full template.
---
## Tips
- **Be specific**: Ask "What's the #1 frustration that brings them to you?" not "What problem do they solve?"
- **Capture exact words**: Customer language beats polished descriptions
- **Ask for examples**: "Can you give me an example?" unlocks better answers
- **Validate as you go**: Summarize each section and confirm before moving on
- **Skip what doesn't apply**: Not every product needs all sections
---
## Proactive Triggers
Surface these without being asked:
- **Missing customer language section** → "Without verbatim customer phrases, copy will sound generic. Can you share 3-5 quotes from customers describing their problem?"
- **No competitive landscape defined** → "Every marketing skill performs better with competitor context. Who are the top 3 alternatives your customers consider?"
- **Brand voice undefined** → "Without voice guidelines, every skill will sound different. Let's define 3-5 adjectives that capture your brand."
- **Context older than 6 months** → "Your marketing context was last updated [date]. Positioning may have shifted — review recommended."
- **No proof points** → "Marketing without proof points is opinion. What metrics, logos, or testimonials can we reference?"
## Output Artifacts
| When you ask for... | You get... |
|---------------------|------------|
| "Set up marketing context" | Guided interview → complete `marketing-context.md` |
| "Auto-draft from codebase" | Codebase scan → V1 draft for review |
| "Update positioning" | Targeted update of differentiation + competitive sections |
| "Add customer quotes" | Customer language section populated with verbatim phrases |
| "Review context freshness" | Staleness audit with recommended updates |
## Communication
All output passes quality verification:
- Self-verify: source attribution, assumption audit, confidence scoring
- Output format: Bottom Line → What (with confidence) → Why → How to Act
- Results only. Every finding tagged: 🟢 verified, 🟡 medium, 🔴 assumed.
## Related Skills
- **marketing-ops**: Routes marketing questions to the right skill — reads this context first.
- **copywriting**: For landing page and web copy. Reads brand voice + customer language from this context.
- **content-strategy**: For planning what content to create. Reads target keywords + personas from this context.
- **marketing-strategy-pmm**: For positioning and GTM strategy. Reads competitive landscape from this context.
- **cs-onboard** (C-Suite): For company-level context. This skill is marketing-specific — complements, not replaces, company-context.md.
FILE:scripts/context_validator.py
#!/usr/bin/env python3
"""Validate marketing context completeness — scores 0-100."""
import json
import re
import sys
from pathlib import Path
SECTIONS = {
"Product Overview": {"required": True, "weight": 10, "markers": ["one-liner", "what it does", "product category", "business model"]},
"Target Audience": {"required": True, "weight": 12, "markers": ["target compan", "decision-maker", "use case", "jobs to be done"]},
"Personas": {"required": False, "weight": 5, "markers": ["persona", "champion", "decision maker"]},
"Problems & Pain Points": {"required": True, "weight": 10, "markers": ["core problem", "fall short", "cost", "tension"]},
"Competitive Landscape": {"required": True, "weight": 10, "markers": ["direct", "competitor", "secondary"]},
"Differentiation": {"required": True, "weight": 10, "markers": ["differentiator", "differently", "why customers choose"]},
"Objections": {"required": False, "weight": 5, "markers": ["objection", "response", "anti-persona"]},
"Switching Dynamics": {"required": False, "weight": 5, "markers": ["push", "pull", "habit", "anxiety"]},
"Customer Language": {"required": True, "weight": 10, "markers": ["verbatim", "words to use", "words to avoid"]},
"Brand Voice": {"required": True, "weight": 8, "markers": ["tone", "style", "personality"]},
"Style Guide": {"required": False, "weight": 3, "markers": ["grammar", "capitalization", "formatting"]},
"Proof Points": {"required": True, "weight": 7, "markers": ["metric", "customer", "testimonial"]},
"Content & SEO": {"required": False, "weight": 3, "markers": ["keyword", "internal link"]},
"Goals": {"required": True, "weight": 2, "markers": ["business goal", "conversion"]}
}
def validate_context(content: str) -> dict:
"""Validate marketing context file and return score."""
content_lower = content.lower()
results = {"sections": {}, "score": 0, "max_score": 100, "missing_required": [], "missing_optional": [], "warnings": []}
total_weight = sum(s["weight"] for s in SECTIONS.values())
earned = 0
for name, config in SECTIONS.items():
section_present = name.lower().replace("& ", "").replace(" ", " ") in content_lower or any(
m in content_lower for m in config["markers"][:2]
)
markers_found = sum(1 for m in config["markers"] if m in content_lower)
markers_total = len(config["markers"])
has_placeholder = bool(re.search(r'\[.*?\]', content[content_lower.find(name.lower()):content_lower.find(name.lower()) + 500] if name.lower() in content_lower else ""))
if section_present and markers_found > 0:
completeness = markers_found / markers_total
if has_placeholder and completeness < 0.5:
completeness *= 0.5 # Penalize unfilled templates
section_score = round(config["weight"] * completeness)
earned += section_score
status = "complete" if completeness >= 0.75 else "partial"
else:
section_score = 0
status = "missing"
if config["required"]:
results["missing_required"].append(name)
else:
results["missing_optional"].append(name)
results["sections"][name] = {
"status": status,
"markers_found": markers_found,
"markers_total": markers_total,
"score": section_score,
"max_score": config["weight"],
"required": config["required"]
}
results["score"] = round((earned / total_weight) * 100)
# Warnings
if "verbatim" not in content_lower and '"' not in content:
results["warnings"].append("No verbatim customer quotes found — copy will sound generic")
if not re.search(r'\d+%|\$\d+|\d+ customer', content_lower):
results["warnings"].append("No metrics or proof points with numbers found")
if "last updated" in content_lower:
date_match = re.search(r'last updated:?\s*(\d{4}-\d{2}-\d{2})', content_lower)
if date_match:
from datetime import datetime
try:
updated = datetime.strptime(date_match.group(1), "%Y-%m-%d")
age_days = (datetime.now() - updated).days
if age_days > 180:
results["warnings"].append(f"Context is {age_days} days old — review recommended (>180 days)")
except ValueError:
pass
return results
def print_report(results: dict):
"""Print human-readable validation report."""
print(f"\n{'='*50}")
print(f"MARKETING CONTEXT VALIDATION")
print(f"{'='*50}")
print(f"\nOverall Score: {results['score']}/100")
print(f"{'🟢 Strong' if results['score'] >= 80 else '🟡 Needs Work' if results['score'] >= 50 else '🔴 Incomplete'}")
print(f"\n{'─'*50}")
print(f"{'Section':<25} {'Status':<10} {'Score':<10}")
print(f"{'─'*50}")
for name, data in results["sections"].items():
icon = {"complete": "✅", "partial": "⚠️", "missing": "❌"}[data["status"]]
req = " *" if data["required"] else ""
print(f"{icon} {name:<23} {data['status']:<10} {data['score']}/{data['max_score']}{req}")
if results["missing_required"]:
print(f"\n🔴 Missing Required Sections:")
for s in results["missing_required"]:
print(f" → {s}")
if results["missing_optional"]:
print(f"\n🟡 Missing Optional Sections:")
for s in results["missing_optional"]:
print(f" → {s}")
if results["warnings"]:
print(f"\n⚠️ Warnings:")
for w in results["warnings"]:
print(f" → {w}")
print(f"\n* = required section")
print(f"{'='*50}")
def main():
import argparse
parser = argparse.ArgumentParser(
description="Validates marketing context completeness. "
"Scores 0-100 based on required and optional section coverage."
)
parser.add_argument(
"file", nargs="?", default=None,
help="Path to a marketing context markdown file. "
"If omitted, runs demo with embedded sample data."
)
parser.add_argument(
"--json", action="store_true",
help="Also output results as JSON."
)
args = parser.parse_args()
if args.file:
filepath = Path(args.file)
if not filepath.exists():
print(f"Error: File not found: {filepath}", file=sys.stderr)
sys.exit(1)
content = filepath.read_text()
else:
# Demo with sample data
content = """# Marketing Context
*Last updated: 2026-01-15*
## Product Overview
**One-liner:** AI-powered mobility analysis for elderly care
**What it does:** Smartphone-based fall risk assessment using computer vision
**Product category:** HealthTech / Digital Health
**Business model:** SaaS, per-facility licensing
## Target Audience
**Target companies:** Care facilities, nursing homes, 50+ beds
**Decision-makers:** Facility directors, quality managers
**Primary use case:** Automated fall risk assessment replacing manual observation
**Jobs to be done:**
- Reduce fall incidents by identifying high-risk residents
- Meet regulatory documentation requirements efficiently
- Give care staff actionable mobility insights
## Problems & Pain Points
**Core problem:** Manual fall risk assessment is subjective, time-consuming, and inconsistent
**Why alternatives fall short:**
- Manual observation takes 30+ minutes per resident
- Paper-based assessments are completed once per quarter at best
**What it costs them:** Falls cost €8,000-12,000 per incident, plus liability
**Emotional tension:** Staff fear missing warning signs, blame after incidents
## Competitive Landscape
**Direct:** Traditional gait labs — $50K+ hardware, need trained staff
**Secondary:** Wearable sensors — low compliance, residents remove them
**Indirect:** Manual observation — subjective, inconsistent
## Differentiation
**Key differentiators:**
- Uses standard smartphone (no special hardware)
- AI-powered analysis (objective, repeatable)
**Why customers choose us:** Fast, affordable, no hardware investment
## Customer Language
**How they describe the problem:**
- "We never know who's going to fall next"
- "The documentation takes forever"
**Words to use:** mobility analysis, fall prevention, care quality
**Words to avoid:** surveillance, monitoring, tracking
## Brand Voice
**Tone:** Professional, empathetic, evidence-based
**Personality:** Trustworthy, innovative, caring
## Proof Points
**Metrics:**
- 80+ care facilities served
- 30% reduction in fall incidents (pilot data)
**Customers:** Major care facility chains in Germany
## Goals
**Business goal:** Expand to 200+ facilities, enter Spain and Netherlands
**Conversion action:** Book a demo
"""
print("[Using embedded sample data — pass a file path for real validation]")
results = validate_context(content)
print_report(results)
if args.json:
print(f"\n{json.dumps(results, indent=2)}")
if __name__ == "__main__":
main()
FILE:templates/marketing-context-template.md
# Marketing Context
*Last updated: [date]*
## Product Overview
**One-liner:** [What you do in one sentence]
**What it does:** [2-3 sentences]
**Product category:** [The "shelf" — how customers search for you]
**Product type:** [SaaS, marketplace, e-commerce, service]
**Business model:** [Pricing model and range]
## Target Audience
**Target companies:** [Industry, size, stage]
**Decision-makers:** [Roles, departments]
**Primary use case:** [The main problem you solve]
**Jobs to be done:**
- [Job 1]
- [Job 2]
- [Job 3]
**Use cases:**
- [Scenario 1]
- [Scenario 2]
## Personas
| Persona | Role | Cares about | Challenge | Value we promise |
|---------|------|-------------|-----------|------------------|
| [Name] | User | | | |
| [Name] | Champion | | | |
| [Name] | Decision Maker | | | |
| [Name] | Financial Buyer | | | |
## Problems & Pain Points
**Core problem:** [What customers face before finding you]
**Why alternatives fall short:**
- [Gap 1]
- [Gap 2]
**What it costs them:** [Time, money, opportunities]
**Emotional tension:** [Stress, fear, doubt]
## Competitive Landscape
| Competitor | Type | How they fall short |
|-----------|------|---------------------|
| [Name] | Direct | [Gap] |
| [Name] | Secondary | [Gap] |
| [Name] | Indirect | [Gap] |
## Differentiation
**Key differentiators:**
- [Differentiator 1]
- [Differentiator 2]
**How we do it differently:** [Approach]
**Why that's better:** [Benefits]
**Why customers choose us:** [Decision drivers]
## Objections
| Objection | Response |
|-----------|----------|
| "[Objection 1]" | [How to address] |
| "[Objection 2]" | [How to address] |
| "[Objection 3]" | [How to address] |
**Anti-persona (NOT a good fit):** [Who should NOT buy this]
## Switching Dynamics
**Push (away from current):** [Frustrations]
**Pull (toward us):** [Attractions]
**Habit (keeping them stuck):** [Inertia]
**Anxiety (about switching):** [Worries]
## Customer Language
**How they describe the problem:**
- "[verbatim quote]"
- "[verbatim quote]"
**How they describe us:**
- "[verbatim quote]"
- "[verbatim quote]"
**Words to use:** [list]
**Words to avoid:** [list]
| Term | Meaning |
|------|---------|
| [Product term] | [Definition] |
## Brand Voice
**Tone:** [professional, casual, playful, authoritative]
**Style:** [direct, conversational, technical]
**Personality:** [3-5 adjectives]
**Voice DO's:** [list]
**Voice DON'T's:** [list]
## Style Guide
**Grammar:** [Key rules]
**Capitalization:** [Conventions]
**Formatting:** [Standards]
**Preferred terms:** [List]
## Proof Points
**Metrics:**
- [Metric 1]
- [Metric 2]
**Customers:** [Notable logos]
**Testimonials:**
> "[quote]" — [Name, Title, Company]
> "[quote]" — [Name, Title, Company]
| Value Theme | Supporting Proof |
|-------------|-----------------|
| [Theme 1] | [Evidence] |
| [Theme 2] | [Evidence] |
## Content & SEO Context
**Target keywords:**
| Cluster | Primary Keyword | Secondary Keywords | Intent |
|---------|----------------|-------------------|--------|
| [Topic 1] | [keyword] | [kw1, kw2] | [informational/commercial] |
**Internal links map:**
| Page | URL | Use for | Anchor text |
|------|-----|---------|-------------|
| [Page name] | [URL] | [Topic] | [Suggested anchor] |
**Writing examples:**
- [URL or file — what makes it good]
## Goals
**Business goal:** [Primary objective]
**Conversion action:** [What you want people to do]
**Current metrics:** [If known]
When the user wants to create competitor comparison or alternative pages for SEO and sales enablement. Also use when the user mentions 'alternative page,' 'v...
---
name: "competitor-alternatives"
description: "When the user wants to create competitor comparison or alternative pages for SEO and sales enablement. Also use when the user mentions 'alternative page,' 'vs page,' 'competitor comparison,' 'comparison page,' '[Product] vs [Product],' '[Product] alternative,' 'competitive landing pages,' 'switch from competitor,' or 'comparison content.' Covers four formats: singular alternative, plural alternatives, you vs competitor, and competitor vs competitor. Emphasizes deep research, modular content architecture, and varied section types beyond feature tables."
license: MIT
metadata:
version: 1.0.0
author: Alireza Rezvani
category: marketing
updated: 2026-03-06
---
# Competitor & Alternative Pages
You are an expert in creating competitor comparison and alternative pages. Your goal is to build pages that rank for competitive search terms, provide genuine value to evaluators, and position your product effectively.
## Initial Assessment
**Check for product marketing context first:**
If `.claude/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
Before creating competitor pages, understand:
1. **Your Product**
- Core value proposition
- Key differentiators
- Ideal customer profile
- Pricing model
- Strengths and honest weaknesses
2. **Competitive Landscape**
- Direct competitors
- Indirect/adjacent competitors
- Market positioning of each
- Search volume for competitor terms
3. **Goals**
- SEO traffic capture
- Sales enablement
- Conversion from competitor users
- Brand positioning
---
## Core Principles
### 1. Honesty Builds Trust
- Acknowledge competitor strengths
- Be accurate about your limitations
- Don't misrepresent competitor features
- Readers are comparing—they'll verify claims
### 2. Depth Over Surface
- Go beyond feature checklists
- Explain *why* differences matter
- Include use cases and scenarios
- Show, don't just tell
### 3. Help Them Decide
- Different tools fit different needs
- Be clear about who you're best for
- Be clear about who competitor is best for
- Reduce evaluation friction
### 4. Modular Content Architecture
- Competitor data should be centralized
- Updates propagate to all pages
- Single source of truth per competitor
---
## Page Formats
### Format 1: [Competitor] Alternative (Singular)
**Search intent**: User is actively looking to switch from a specific competitor
**URL pattern**: `/alternatives/[competitor]` or `/[competitor]-alternative`
**Target keywords**: "[Competitor] alternative", "alternative to [Competitor]", "switch from [Competitor]"
**Page structure**:
1. Why people look for alternatives (validate their pain)
2. Summary: You as the alternative (quick positioning)
3. Detailed comparison (features, service, pricing)
4. Who should switch (and who shouldn't)
5. Migration path
6. Social proof from switchers
7. CTA
---
### Format 2: [Competitor] Alternatives (Plural)
**Search intent**: User is researching options, earlier in journey
**URL pattern**: `/alternatives/[competitor]-alternatives`
**Target keywords**: "[Competitor] alternatives", "best [Competitor] alternatives", "tools like [Competitor]"
**Page structure**:
1. Why people look for alternatives (common pain points)
2. What to look for in an alternative (criteria framework)
3. List of alternatives (you first, but include real options)
4. Comparison table (summary)
5. Detailed breakdown of each alternative
6. Recommendation by use case
7. CTA
**Important**: Include 4-7 real alternatives. Being genuinely helpful builds trust and ranks better.
---
### Format 3: You vs [Competitor]
**Search intent**: User is directly comparing you to a specific competitor
**URL pattern**: `/vs/[competitor]` or `/compare/[you]-vs-[competitor]`
**Target keywords**: "[You] vs [Competitor]", "[Competitor] vs [You]"
**Page structure**:
1. TL;DR summary (key differences in 2-3 sentences)
2. At-a-glance comparison table
3. Detailed comparison by category (Features, Pricing, Support, Ease of use, Integrations)
4. Who [You] is best for
5. Who [Competitor] is best for (be honest)
6. What customers say (testimonials from switchers)
7. Migration support
8. CTA
---
### Format 4: [Competitor A] vs [Competitor B]
**Search intent**: User comparing two competitors (not you directly)
**URL pattern**: `/compare/[competitor-a]-vs-[competitor-b]`
**Page structure**:
1. Overview of both products
2. Comparison by category
3. Who each is best for
4. The third option (introduce yourself)
5. Comparison table (all three)
6. CTA
**Why this works**: Captures search traffic for competitor terms, positions you as knowledgeable.
---
## Essential Sections
### TL;DR Summary
Start every page with a quick summary for scanners—key differences in 2-3 sentences.
### Paragraph Comparisons
Go beyond tables. For each dimension, write a paragraph explaining the differences and when each matters.
### Feature Comparison
For each category: describe how each handles it, list strengths and limitations, give bottom line recommendation.
### Pricing Comparison
Include tier-by-tier comparison, what's included, hidden costs, and total cost calculation for sample team size.
### Who It's For
Be explicit about ideal customer for each option. Honest recommendations build trust.
### Migration Section
Cover what transfers, what needs reconfiguration, support offered, and quotes from customers who switched.
**For detailed templates**: See [references/templates.md](references/templates.md)
---
## Content Architecture
### Centralized Competitor Data
Create a single source of truth for each competitor with:
- Positioning and target audience
- Pricing (all tiers)
- Feature ratings
- Strengths and weaknesses
- Best for / not ideal for
- Common complaints (from reviews)
- Migration notes
**For data structure and examples**: See [references/content-architecture.md](references/content-architecture.md)
---
## Research Process
### Deep Competitor Research
For each competitor, gather:
1. **Product research**: Sign up, use it, document features/UX/limitations
2. **Pricing research**: Current pricing, what's included, hidden costs
3. **Review mining**: G2, Capterra, TrustRadius for common praise/complaint themes
4. **Customer feedback**: Talk to customers who switched (both directions)
5. **Content research**: Their positioning, their comparison pages, their changelog
### Ongoing Updates
- **Quarterly**: Verify pricing, check for major feature changes
- **When notified**: Customer mentions competitor change
- **Annually**: Full refresh of all competitor data
---
## SEO Considerations
### Keyword Targeting
| Format | Primary Keywords |
|--------|-----------------|
| Alternative (singular) | [Competitor] alternative, alternative to [Competitor] |
| Alternatives (plural) | [Competitor] alternatives, best [Competitor] alternatives |
| You vs Competitor | [You] vs [Competitor], [Competitor] vs [You] |
| Competitor vs Competitor | [A] vs [B], [B] vs [A] |
### Internal Linking
- Link between related competitor pages
- Link from feature pages to relevant comparisons
- Create hub page linking to all competitor content
### Schema Markup
Consider FAQ schema for common questions like "What is the best alternative to [Competitor]?"
---
## Output Format
### Competitor Data File
Complete competitor profile in YAML format for use across all comparison pages.
### Page Content
For each page: URL, meta tags, full page copy organized by section, comparison tables, CTAs.
### Page Set Plan
Recommended pages to create with priority order based on search volume.
---
## Task-Specific Questions
1. What are common reasons people switch to you?
2. Do you have customer quotes about switching?
3. What's your pricing vs. competitors?
4. Do you offer migration support?
---
## Proactive Triggers
Proactively offer competitor page creation when:
1. **Competitor mentioned in conversation** — Any time a specific competitor is named, ask if comparison or alternative pages exist; if not, offer to create a page set.
2. **Sales team friction** — User mentions prospects comparing them to a specific tool; immediately offer a vs-page for sales enablement.
3. **SEO gap identified** — Keyword research shows competitor-branded terms with no coverage; propose a full alternative page set with prioritized build order.
4. **Switcher testimonial available** — When a customer quote about switching surfaces, offer to build a migration-focused alternative page around it.
5. **Pricing page review** — When reviewing pricing, note that pricing comparison tables belong on dedicated competitor pages, not the pricing page itself.
---
## Output Artifacts
| Artifact | Format | Description |
|----------|--------|-------------|
| Competitor Intelligence File | YAML data file | Centralized competitor profile: pricing, features, weaknesses, review themes |
| Page Set Plan | Prioritized list | Ranked list of pages to build with target keywords and search volume estimates |
| Alternative Page (Singular) | Full page copy | Complete `/[competitor]-alternative` page with all sections |
| Vs Page | Full page copy | Complete `/vs/[competitor]` page with comparison table and CTA |
| Migration Guide Section | Markdown block | Reusable migration copy for inclusion across multiple pages |
---
## Communication
All competitor page outputs should be factually accurate, legally safe (no false claims), and fair to competitors. Acknowledge genuine competitor strengths — pages that only disparage competitors lose credibility with evaluators. Reference `marketing-context` for ICP and positioning before writing any comparison copy. Quality bar: every claim must be verifiable from public sources or customer quotes.
---
## Related Skills
- **seo-audit** — USE to validate that competitor pages meet on-page SEO requirements before publishing; NOT as a replacement for the keyword strategy built here.
- **copywriting** — USE for writing the narrative sections and CTAs on comparison pages; NOT when the task is purely competitor research and architecture.
- **content-strategy** — USE when planning a full competitive content program across multiple pages; NOT for single-page execution.
- **competitive-intel** — USE when C-level strategic competitive analysis is needed beyond page creation; NOT for tactical page writing.
- **marketing-context** — USE as foundation before any competitor page work to align positioning; always load first.
FILE:references/content-architecture.md
# Content Architecture for Competitor Pages
How to structure and maintain competitor data for scalable comparison pages.
## Centralized Competitor Data
Create a single source of truth for each competitor:
```
competitor_data/
├── notion.md
├── airtable.md
├── monday.md
└── ...
```
---
## Competitor Data Template
Per competitor, document:
```yaml
name: Notion
website: notion.so
tagline: "The all-in-one workspace"
founded: 2016
headquarters: San Francisco
# Positioning
primary_use_case: "docs + light databases"
target_audience: "teams wanting flexible workspace"
market_position: "premium, feature-rich"
# Pricing
pricing_model: per-seat
free_tier: true
free_tier_limits: "limited blocks, 1 user"
starter_price: $8/user/month
business_price: $15/user/month
enterprise: custom
# Features (rate 1-5 or describe)
features:
documents: 5
databases: 4
project_management: 3
collaboration: 4
integrations: 3
mobile_app: 3
offline_mode: 2
api: 4
# Strengths (be honest)
strengths:
- Extremely flexible and customizable
- Beautiful, modern interface
- Strong template ecosystem
- Active community
# Weaknesses (be fair)
weaknesses:
- Can be slow with large databases
- Learning curve for advanced features
- Limited automations compared to dedicated tools
- Offline mode is limited
# Best for
best_for:
- Teams wanting all-in-one workspace
- Content-heavy workflows
- Documentation-first teams
- Startups and small teams
# Not ideal for
not_ideal_for:
- Complex project management needs
- Large databases (1000s of rows)
- Teams needing robust offline
- Enterprise with strict compliance
# Common complaints (from reviews)
common_complaints:
- "Gets slow with lots of content"
- "Hard to find things as workspace grows"
- "Mobile app is clunky"
# Migration notes
migration_from:
difficulty: medium
data_export: "Markdown, CSV, HTML"
what_transfers: "Pages, databases"
what_doesnt: "Automations, integrations setup"
time_estimate: "1-3 days for small team"
```
---
## Your Product Data
Same structure for yourself—be honest:
```yaml
name: [Your Product]
# ... same fields
strengths:
- [Your real strengths]
weaknesses:
- [Your honest weaknesses]
best_for:
- [Your ideal customers]
not_ideal_for:
- [Who should use something else]
```
---
## Page Generation
Each page pulls from centralized data:
- **[Competitor] Alternative page**: Pulls competitor data + your data
- **[Competitor] Alternatives page**: Pulls competitor data + your data + other alternatives
- **You vs [Competitor] page**: Pulls your data + competitor data
- **[A] vs [B] page**: Pulls both competitor data + your data
**Benefits**:
- Update competitor pricing once, updates everywhere
- Add new feature comparison once, appears on all pages
- Consistent accuracy across pages
- Easier to maintain at scale
---
## Index Page Structure
### Alternatives Index
**URL**: `/alternatives` or `/alternatives/index`
**Purpose**: Lists all "[Competitor] Alternative" pages
**Page structure**:
1. Headline: "[Your Product] as an Alternative"
2. Brief intro on why people switch to you
3. List of all alternative pages with:
- Competitor name/logo
- One-line summary of key differentiator vs. that competitor
- Link to full comparison
4. Common reasons people switch (aggregated)
5. CTA
**Example**:
```markdown
## Explore [Your Product] as an Alternative
Looking to switch? See how [Your Product] compares to the tools you're evaluating:
- **[Notion Alternative](/alternatives/notion)** — Better for teams who need [X]
- **[Airtable Alternative](/alternatives/airtable)** — Better for teams who need [Y]
- **[Monday Alternative](/alternatives/monday)** — Better for teams who need [Z]
```
---
### Vs Comparisons Index
**URL**: `/vs` or `/compare`
**Purpose**: Lists all "You vs [Competitor]" and "[A] vs [B]" pages
**Page structure**:
1. Headline: "Compare [Your Product]"
2. Section: "[Your Product] vs Competitors" — list of direct comparisons
3. Section: "Head-to-Head Comparisons" — list of [A] vs [B] pages
4. Brief methodology note
5. CTA
---
### Index Page Best Practices
**Keep them updated**: When you add a new comparison page, add it to the relevant index.
**Internal linking**:
- Link from index → individual pages
- Link from individual pages → back to index
- Cross-link between related comparisons
**SEO value**:
- Index pages can rank for broad terms like "project management tool comparisons"
- Pass link equity to individual comparison pages
- Help search engines discover all comparison content
**Sorting options**:
- By popularity (search volume)
- Alphabetically
- By category/use case
- By date added (show freshness)
**Include on index pages**:
- Last updated date for credibility
- Number of pages/comparisons available
- Quick filters if you have many comparisons
---
## Footer Navigation
The site footer appears on all marketing pages, making it a powerful internal linking opportunity for competitor pages.
### Option 1: Link to Index Pages (Minimum)
At minimum, add links to your comparison index pages in the footer:
```
Footer
├── Compare
│ ├── Alternatives → /alternatives
│ └── Comparisons → /vs
```
This ensures every marketing page passes link equity to your comparison content hub.
### Option 2: Footer Columns by Format (Recommended for SEO)
For stronger internal linking, create dedicated footer columns for each format you've built, linking directly to your top competitors:
```
Footer
├── [Product] vs ├── Alternatives to ├── Compare
│ ├── vs Notion │ ├── Notion Alternative │ ├── Notion vs Airtable
│ ├── vs Airtable │ ├── Airtable Alternative │ ├── Monday vs Asana
│ ├── vs Monday │ ├── Monday Alternative │ ├── Notion vs Monday
│ ├── vs Asana │ ├── Asana Alternative │ ├── ...
│ ├── vs Clickup │ ├── Clickup Alternative │ └── View all →
│ ├── ... │ ├── ... │
│ └── View all → │ └── View all → │
```
**Guidelines**:
- Include up to 8 links per column (top competitors by search volume)
- Add "View all" link to the full index page
- Only create columns for formats you've actually built pages for
- Prioritize competitors with highest search volume
### Why Footer Links Matter
1. **Sitewide distribution**: Footer links appear on every marketing page, passing link equity from your entire site to comparison content
2. **Crawl efficiency**: Search engines discover all comparison pages quickly
3. **User discovery**: Visitors evaluating your product can easily find comparisons
4. **Competitive positioning**: Signals to search engines that you're a key player in the space
### Implementation Notes
- Update footer when adding new high-priority comparison pages
- Keep footer clean—don't list every comparison, just the top ones
- Match column headers to your URL structure (e.g., "vs" column → `/vs/` URLs)
- Consider mobile: columns may stack, so order by priority
FILE:references/templates.md
# Section Templates for Competitor Pages
Ready-to-use templates for each section of competitor comparison pages.
## TL;DR Summary
Start every page with a quick summary for scanners:
```markdown
**TL;DR**: [Competitor] excels at [strength] but struggles with [weakness].
[Your product] is built for [your focus], offering [key differentiator].
Choose [Competitor] if [their ideal use case]. Choose [You] if [your ideal use case].
```
---
## Paragraph Comparison (Not Just Tables)
For each major dimension, write a paragraph:
```markdown
## Features
[Competitor] offers [description of their feature approach].
Their strength is [specific strength], which works well for [use case].
However, [limitation] can be challenging for [user type].
[Your product] takes a different approach with [your approach].
This means [benefit], though [honest tradeoff].
Teams who [specific need] often find this more effective.
```
---
## Feature Comparison Section
Go beyond checkmarks:
```markdown
## Feature Comparison
### [Feature Category]
**[Competitor]**: [2-3 sentence description of how they handle this]
- Strengths: [specific]
- Limitations: [specific]
**[Your product]**: [2-3 sentence description]
- Strengths: [specific]
- Limitations: [specific]
**Bottom line**: Choose [Competitor] if [scenario]. Choose [You] if [scenario].
```
---
## Pricing Comparison Section
```markdown
## Pricing
| | [Competitor] | [Your Product] |
|---|---|---|
| Free tier | [Details] | [Details] |
| Starting price | $X/user/mo | $X/user/mo |
| Business tier | $X/user/mo | $X/user/mo |
| Enterprise | Custom | Custom |
**What's included**: [Competitor]'s $X plan includes [features], while
[Your product]'s $X plan includes [features].
**Total cost consideration**: Beyond per-seat pricing, consider [hidden costs,
add-ons, implementation]. [Competitor] charges extra for [X], while
[Your product] includes [Y] in base pricing.
**Value comparison**: For a 10-person team, [Competitor] costs approximately
$X/year while [Your product] costs $Y/year, with [key differences in what you get].
```
---
## Service & Support Comparison
```markdown
## Service & Support
| | [Competitor] | [Your Product] |
|---|---|---|
| Documentation | [Quality assessment] | [Quality assessment] |
| Response time | [SLA if known] | [Your SLA] |
| Support channels | [List] | [List] |
| Onboarding | [What they offer] | [What you offer] |
| CSM included | [At what tier] | [At what tier] |
**Support quality**: Based on [G2/Capterra reviews, your research],
[Competitor] support is described as [assessment]. Common feedback includes
[quotes or themes].
[Your product] offers [your support approach]. [Specific differentiator like
response time, dedicated CSM, implementation help].
```
---
## Who It's For Section
```markdown
## Who Should Choose [Competitor]
[Competitor] is the right choice if:
- [Specific use case or need]
- [Team type or size]
- [Workflow or requirement]
- [Budget or priority]
**Ideal [Competitor] customer**: [Persona description in 1-2 sentences]
## Who Should Choose [Your Product]
[Your product] is built for teams who:
- [Specific use case or need]
- [Team type or size]
- [Workflow or requirement]
- [Priority or value]
**Ideal [Your product] customer**: [Persona description in 1-2 sentences]
```
---
## Migration Section
```markdown
## Switching from [Competitor]
### What transfers
- [Data type]: [How easily, any caveats]
- [Data type]: [How easily, any caveats]
### What needs reconfiguration
- [Thing]: [Why and effort level]
- [Thing]: [Why and effort level]
### Migration support
We offer [migration support details]:
- [Free data import tool / white-glove migration]
- [Documentation / migration guide]
- [Timeline expectation]
- [Support during transition]
### What customers say about switching
> "[Quote from customer who switched]"
> — [Name], [Role] at [Company]
```
---
## Social Proof Section
Focus on switchers:
```markdown
## What Customers Say
### Switched from [Competitor]
> "[Specific quote about why they switched and outcome]"
> — [Name], [Role] at [Company]
> "[Another quote]"
> — [Name], [Role] at [Company]
### Results after switching
- [Company] saw [specific result]
- [Company] reduced [metric] by [amount]
```
---
## Comparison Table Best Practices
### Beyond Checkmarks
Instead of:
| Feature | You | Competitor |
|---------|-----|-----------|
| Feature A | ✓ | ✓ |
| Feature B | ✓ | ✗ |
Do this:
| Feature | You | Competitor |
|---------|-----|-----------|
| Feature A | Full support with [detail] | Basic support, [limitation] |
| Feature B | [Specific capability] | Not available |
### Organize by Category
Group features into meaningful categories:
- Core functionality
- Collaboration
- Integrations
- Security & compliance
- Support & service
### Include Ratings Where Useful
| Category | You | Competitor | Notes |
|----------|-----|-----------|-------|
| Ease of use | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | [Brief note] |
| Feature depth | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | [Brief note] |
FILE:scripts/comparison_matrix_builder.py
#!/usr/bin/env python3
"""
comparison_matrix_builder.py — Competitive Feature Comparison Matrix Builder
100% stdlib, no pip installs required.
Usage:
python3 comparison_matrix_builder.py # demo mode
python3 comparison_matrix_builder.py --input matrix.json
python3 comparison_matrix_builder.py --input matrix.json --json
python3 comparison_matrix_builder.py --input matrix.json --markdown > comparison.md
matrix.json format:
{
"your_product": "YourProduct",
"features": [
{
"name": "SSO / SAML",
"category": "Security",
"your_status": "full", # full | partial | no | planned
"competitors": {
"CompetitorA": "no",
"CompetitorB": "partial",
"CompetitorC": "full"
},
"notes": "Enterprise tier only" # optional
}
]
}
"""
import argparse
import json
import sys
from collections import defaultdict
# ---------------------------------------------------------------------------
# Status helpers
# ---------------------------------------------------------------------------
STATUS_SCORE = {
"full": 2,
"partial": 1,
"no": 0,
"planned": 0, # planned ≠ shipped; conservative scoring
}
STATUS_LABEL = {
"full": "✅",
"partial": "🔶",
"no": "❌",
"planned": "🗓",
}
STATUS_TEXT = {
"full": "Full",
"partial": "Partial",
"no": "No",
"planned": "Planned",
}
FEATURE_IMPORTANCE = {
# Generic defaults — override per-feature with "weight" in JSON
"default": 1,
}
# ---------------------------------------------------------------------------
# Core builder
# ---------------------------------------------------------------------------
def normalise_status(s: str) -> str:
s = (s or "no").strip().lower()
return s if s in STATUS_SCORE else "no"
def build_matrix(data: dict) -> dict:
your_product = data.get("your_product", "Your Product")
features = data.get("features", [])
if not features:
raise ValueError("No features provided in input.")
# Collect competitor names (ordered, deduplicated)
competitors = []
seen = set()
for f in features:
for c in f.get("competitors", {}):
if c not in seen:
competitors.append(c)
seen.add(c)
categories = sorted(set(f.get("category", "General") for f in features))
# --- per-feature analysis ---
feature_rows = []
for f in features:
fname = f.get("name", "?")
category = f.get("category", "General")
weight = f.get("weight", 1)
your_raw = normalise_status(f.get("your_status", "no"))
your_s = STATUS_SCORE[your_raw]
comp_raw = {c: normalise_status(f.get("competitors", {}).get(c, "no"))
for c in competitors}
comp_s = {c: STATUS_SCORE[comp_raw[c]] for c in competitors}
you_win = all(your_s > comp_s[c] for c in competitors) if competitors else False
you_lose = any(your_s < comp_s[c] for c in competitors)
your_max = max(comp_s.values()) if comp_s else 0
advantage = your_s - your_max # positive = you're better overall
feature_rows.append({
"name": fname,
"category": category,
"weight": weight,
"your_status": your_raw,
"your_score": your_s,
"competitors": comp_raw,
"comp_scores": comp_s,
"you_win": you_win,
"you_lose": you_lose,
"advantage": advantage,
"notes": f.get("notes", ""),
})
# --- competitive scores per competitor ---
comp_scores = {}
for c in competitors:
wins = sum(1 for r in feature_rows if r["your_score"] > r["comp_scores"].get(c, 0))
ties = sum(1 for r in feature_rows if r["your_score"] == r["comp_scores"].get(c, 0))
losses = sum(1 for r in feature_rows if r["your_score"] < r["comp_scores"].get(c, 0))
total = len(feature_rows)
score = round((wins / total) * 100) if total else 0
comp_scores[c] = {
"wins": wins, "ties": ties, "losses": losses,
"win_pct": score,
"verdict": _verdict(score),
}
# Overall competitive score (average win% across all competitors)
overall_win_pct = (
round(sum(v["win_pct"] for v in comp_scores.values()) / len(comp_scores))
if comp_scores else 0
)
# Advantages and gaps
advantages = [r["name"] for r in feature_rows if r["advantage"] > 0]
gaps = [r["name"] for r in feature_rows if r["advantage"] < 0]
parity = [r["name"] for r in feature_rows if r["advantage"] == 0]
return {
"meta": {
"your_product": your_product,
"competitors": competitors,
"categories": categories,
"total_features": len(feature_rows),
"overall_win_pct": overall_win_pct,
"verdict": _verdict(overall_win_pct),
},
"competitor_scores": comp_scores,
"advantages": advantages,
"gaps": gaps,
"parity": parity,
"features": feature_rows,
}
def _verdict(win_pct: int) -> str:
if win_pct >= 70: return "Strong advantage"
if win_pct >= 50: return "Slight advantage"
if win_pct >= 35: return "Competitive parity"
return "Trailing"
# ---------------------------------------------------------------------------
# Markdown output
# ---------------------------------------------------------------------------
def build_markdown(result: dict) -> str:
m = result["meta"]
rows = result["features"]
comp = m["competitors"]
lines = []
lines.append(f"# Feature Comparison: {m['your_product']} vs Competitors\n")
lines.append(f"_Generated by comparison_matrix_builder.py — {m['total_features']} features, "
f"{len(comp)} competitor(s)_\n")
# Summary table
lines.append("## Competitive Score Summary\n")
lines.append("| Competitor | You Win | Tie | You Lose | Win % | Verdict |")
lines.append("|---|---|---|---|---|---|")
for c, s in result["competitor_scores"].items():
lines.append(f"| {c} | {s['wins']} | {s['ties']} | {s['losses']} | "
f"**{s['win_pct']}%** | {s['verdict']} |")
lines.append(f"\n**Overall win rate: {m['overall_win_pct']}% — {m['verdict']}**\n")
# Feature matrix by category
lines.append("## Feature Matrix\n")
header = f"| Feature | {m['your_product']} | " + " | ".join(comp) + " | Notes |"
sep = "|---|---|" + "|".join(["---"] * len(comp)) + "|---|"
lines.append(header)
lines.append(sep)
current_cat = None
for r in rows:
cat = r["category"]
if cat != current_cat:
lines.append(f"| **{cat}** | | " + " | ".join([""] * len(comp)) + " | |")
current_cat = cat
you_icon = STATUS_LABEL[r["your_status"]]
comp_icons = " | ".join(STATUS_LABEL[r["competitors"].get(c, "no")] for c in comp)
note = r["notes"] or ""
# Highlight row if it's a unique advantage
fname = f"**{r['name']}**" if r["advantage"] > 0 else r["name"]
lines.append(f"| {fname} | {you_icon} | {comp_icons} | {note} |")
lines.append("")
# Advantages
if result["advantages"]:
lines.append("## ✅ Your Advantages\n")
for a in result["advantages"]:
lines.append(f"- {a}")
lines.append("")
# Gaps
if result["gaps"]:
lines.append("## ⚠️ Feature Gaps (competitors ahead)\n")
for g in result["gaps"]:
lines.append(f"- {g}")
lines.append("")
# Legend
lines.append("## Legend\n")
for k, v in STATUS_LABEL.items():
lines.append(f"- {v} {STATUS_TEXT[k]}")
lines.append("")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Pretty terminal output
# ---------------------------------------------------------------------------
def pretty_print(result: dict) -> None:
m = result["meta"]
print("\n" + "=" * 70)
print(f" COMPETITIVE MATRIX: {m['your_product'].upper()} vs {', '.join(m['competitors'])}")
print("=" * 70)
print(f"\n Total features analysed : {m['total_features']}")
print(f" Overall win rate : {m['overall_win_pct']}% ({m['verdict']})")
print(f"\n{'─'*70}")
print(f" {'COMPETITOR':<22} {'WIN%':>5} {'WINS':>5} {'TIES':>5} {'LOSSES':>7} VERDICT")
print(f"{'─'*70}")
for c, s in result["competitor_scores"].items():
bar = "█" * (s["win_pct"] // 10) + "░" * (10 - s["win_pct"] // 10)
print(f" {c:<22} {s['win_pct']:>4}% {s['wins']:>5} {s['ties']:>5} "
f"{s['losses']:>7} {bar} {s['verdict']}")
print(f"\n{'─'*70}")
col_w = 20
header = f" {'FEATURE':<28} | {'YOU':^8}"
for c in m["competitors"]:
header += f" | {c[:8]:^8}"
print(header)
print("─" * (30 + 11 * (1 + len(m["competitors"]))))
current_cat = None
for r in result["features"]:
if r["category"] != current_cat:
print(f"\n [{r['category']}]")
current_cat = r["category"]
you_icon = STATUS_LABEL[r["your_status"]]
line = f" {' '+r['name']:<28} | {you_icon:^8}"
for c in m["competitors"]:
ci = STATUS_LABEL[r["competitors"].get(c, "no")]
line += f" | {ci:^8}"
if r["advantage"] > 0:
line += " ← advantage"
elif r["advantage"] < 0:
line += " ← gap"
print(line)
print(f"\n ✅ YOUR ADVANTAGES ({len(result['advantages'])} features)")
for a in result["advantages"]:
print(f" • {a}")
print(f"\n ⚠️ FEATURE GAPS ({len(result['gaps'])} features)")
for g in result["gaps"]:
print(f" • {g}")
print(f"\n Legend: {STATUS_LABEL['full']} Full {STATUS_LABEL['partial']} Partial "
f"{STATUS_LABEL['no']} No {STATUS_LABEL['planned']} Planned\n")
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
DEMO_DATA = {
"your_product": "SwiftBase",
"features": [
{"name": "SSO / SAML", "category": "Security", "weight": 3, "your_status": "full", "competitors": {"AcmeSaaS": "no", "ProStack": "partial"}, "notes": "All plans"},
{"name": "2FA / MFA", "category": "Security", "weight": 3, "your_status": "full", "competitors": {"AcmeSaaS": "full", "ProStack": "full"}, "notes": ""},
{"name": "SOC 2 Type II", "category": "Security", "weight": 3, "your_status": "planned", "competitors": {"AcmeSaaS": "full", "ProStack": "no"}, "notes": "Q3 target"},
{"name": "Role-based access", "category": "Security", "weight": 2, "your_status": "full", "competitors": {"AcmeSaaS": "partial", "ProStack": "full"}, "notes": ""},
{"name": "REST API", "category": "Integrations", "weight": 3, "your_status": "full", "competitors": {"AcmeSaaS": "full", "ProStack": "full"}, "notes": ""},
{"name": "GraphQL API", "category": "Integrations", "weight": 2, "your_status": "full", "competitors": {"AcmeSaaS": "no", "ProStack": "partial"}, "notes": ""},
{"name": "Zapier Integration", "category": "Integrations", "weight": 2, "your_status": "partial", "competitors": {"AcmeSaaS": "full", "ProStack": "full"}, "notes": "10 zaps only"},
{"name": "Webhooks", "category": "Integrations", "weight": 2, "your_status": "full", "competitors": {"AcmeSaaS": "full", "ProStack": "no"}, "notes": ""},
{"name": "Custom domain", "category": "Branding", "weight": 2, "your_status": "full", "competitors": {"AcmeSaaS": "partial", "ProStack": "full"}, "notes": ""},
{"name": "White-label / rebrand","category": "Branding", "weight": 2, "your_status": "full", "competitors": {"AcmeSaaS": "no", "ProStack": "partial"}, "notes": "Agency plan"},
{"name": "Priority support", "category": "Support", "weight": 2, "your_status": "full", "competitors": {"AcmeSaaS": "partial", "ProStack": "full"}, "notes": "24/7"},
{"name": "Dedicated CSM", "category": "Support", "weight": 2, "your_status": "no", "competitors": {"AcmeSaaS": "full", "ProStack": "full"}, "notes": "Enterprise only"},
{"name": "SLA guarantee", "category": "Support", "weight": 3, "your_status": "no", "competitors": {"AcmeSaaS": "full", "ProStack": "no"}, "notes": "Roadmap"},
],
}
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def parse_args():
parser = argparse.ArgumentParser(
description="Build a competitive feature comparison matrix (stdlib only).",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--input", type=str, default=None,
help="Path to JSON input file")
parser.add_argument("--json", action="store_true",
help="Output analysis as JSON")
parser.add_argument("--markdown", action="store_true",
help="Output comparison table as Markdown")
return parser.parse_args()
def main():
args = parse_args()
if args.input:
with open(args.input) as f:
data = json.load(f)
else:
print("🔬 DEMO MODE — using sample SaaS product matrix\n", file=sys.stderr)
data = DEMO_DATA
result = build_matrix(data)
if args.json:
# Serialise (remove non-JSON-safe keys)
print(json.dumps(result, indent=2))
elif args.markdown:
print(build_markdown(result))
else:
pretty_print(result)
print("\n💡 TIP: Re-run with --markdown to get a copyable Markdown table.\n")
if __name__ == "__main__":
main()