Operator guide

End-to-end flow for the Castellan outreach system. localhost:3000 is your command center — funnel stats, queue review, template editing, pipeline buttons, conversations.

Where things run

outreach.castellan.so = engine room. Cron endpoints (GitHub Actions hits them every 15 min), click-tracking redirect at r.castellan.so, Gmail send. You don't visit it.

localhost:3000 = cockpit. A launchd agent auto-starts it on login (./scripts/install-dev-server-launchd.sh).

Vault vs. database

Vault (~/Developer/discovery-vault/) = thinking layer. Research, synthesis, interaction transcripts, T1 templates A/B/C/D. Authoritative for who is in the pipeline.

Database (Postgres on Neon) = operational layer. Sends, replies, scheduled_for, statuses. Authoritative for what the system did. Don't hand-edit pipeline state.

Handoff: vault → DB via build:dossiers + draft:owner-emails. DB → vault via sync:vault (read-only audit trail).

1.

Top of funnel — discovering new PMCs and listings

Before anything else can happen, you need data inthe vault. This is the "run periodically" layer — none of these are dashboard buttons because they're slow, use paid APIs, or require human review of the output.

  • Scrape Zillow for stale listings under each tracked PMC. npm run scrape:zillow. Iterates the PMC list, searches Zillow for each, parses the listings, writes/updates discovery-vault/leads/<pmc>__long-beach-ca.md. Uses Browserbase under the hood (anti-bot resilient). Run weekly-ish to refresh DOM counts and find new vacancies.
  • Detect each PMC's tech platform stack. npm run backfill:tech-platforms:kimi (LLM-driven from PMC website) or :browserbase (mystery-shopper inquiry to detect what auto-responds). Outputs pms_detected in lead frontmatter — used as an ICP signal (Tenant Turner / Property Meld / etc. = mid-market PM, our wedge).
  • Skip-trace owners for stale-listing addresses. npm run find:owners:batchdata + find:owner-contacts:batchdata (paid; primary path). Or fallbacks: find:propstream:skip-trace, find:apify:skip-trace, find:la-assessor. Writes owner_contacts[] into each lead file.
  • Discover new PMCs from IREM SoCal directory. npm run scrape:irem-socal npm run match:irem-prospects. Use occasionally to find PMCs we haven't scraped yet.
  • Add a PMC by hand. If you spot a target manually (referral, news, etc.), create a new lead file in discovery-vault/leads/<slug>.md with at minimum: name, website, principals[]. Then re-run the scrape + skip-trace commands to populate the rest.

All of these write to the vault filesystem (markdown files). The dashboard's Coverage section reads from those files to show the funnel + per-PMC engagement.

2.

Edit leads + curate the cohort (vault)

Per-PMC lead files live at discovery-vault/leads/. Each has YAML frontmatter (PMC metadata + owner_contacts[]) and prose body (listings, principals, notes).

Hand-touch when:

  • Marking do_not_contact_overall: true on a contact
  • Adding a hand-found owner to owner_contacts[]
  • Adjusting principals[] after a discovery call
  • Recording an interaction note in discovery-vault/interactions/
3.

Build dossiers (one per owner)

Click Rebuild dossiers on the home page (or run npm run build:dossiers). Reads each lead file, dedupes by owner, picks the longest-DOM listing as the primary hook, classifies archetype (A/B/C/D), scores quality, ranks for first-batch selection. Writes per-owner dossiers to discovery-vault/leads/_outreach_drafts/.

Multi-property handling: one dossier per owner even if they have multiple stale units. The body references the longest-DOM unit + alludes to "any of your other stale units" generically.

Operator overrides preserved: if you flip selected_for_first_batch in /dossiers (or directly in the dossier file), rebuilds preserve that. Same for primary_template.

4.

Generate T1 drafts and queue them in the DB

Click Queue T1 drafts on the home page (or run npm run draft:owner-emails). For each first-batch dossier: picks the right vault template (B/D), upserts a prospect row, inserts a T1 send with status=queued.

Idempotent — won't double-insert. Refresh queued bodies button (or --refresh-queued flag) updates body+subject on already-queued T1s, useful after editing the vault template.

T2/T3/T4 are NOT created here. The cron creates them after T1 sends, using the sequence_steps defaults editable at /campaigns/lb-owner.

5.

Review the queue (daily flow)

Go to /queue. Per-draft actions:

  • Approve — schedules for next business day at 8:27 AM PT
  • Schedule… — pick a custom date/time (PT)
  • Edit — modify subject or body before approval
  • Skip — never send this one

Bulk: check rows → Approve N or Schedule…. Bulk schedule auto-staggers 30s apart.

⚠ Drafts queued >48h ago show a stale-warning banner with a Zillow search link — verify the listing is still up before approving.

⚠ Bulk-approving 2+ owners under the same PMC for the same day creates a "cluster" the PMC principal will notice. The picker warns you. Spread across days using Schedule….

6.

Cron sends (every 15 min) + reply handling

GitHub Actions runs cron-send-approved every 15 min, calls Gmail for any scheduled_for ≤ NOW()approved row. After T1 sends, the cron auto-creates T2 (+3d), T3 (+7d), T4 (+18d) using the campaign's sequence_steps defaults + ±60 min jitter.

cron-poll-replies runs every 15 min, polls Gmail inbox, classifies replies (keyword-only — catches bounces + unsubscribes; everything else stays null until the LLM classifier picks them up).

LLM reply classifier (Claude via your Max plan, not the API): a launchd job runs classify:replies every 30 min on your MacBook, classifies any null replies. Install with ./scripts/install-classify-launchd.sh. The dashboard also has a manual Classify replies button.

View threaded replies at /conversations. Each reply has a dropdown for manual reclassification — useful when the LLM gets one wrong. Picking unsubscribe sets the prospect to paused (sequence never resumes).

7.

Recovery — when something goes wrong

  • Approved by mistake? Approved tab → click row → Unapprove (back to queued) or Cancel send (skipped).
  • Wrong send time? Approved tab → row → Reschedule…
  • Want to test the cron without waiting? Schedule… → ASAP → fires at the next 15-min tick.
  • Template change after T1 sent? Edit the vault template at /templates, run draft:owner-emails -- --refresh-queuedto push to already-queued T1s. Already-sent T1s don't change.
  • Reply misclassified? /conversations → dropdown next to the classification → pick correct one. Cascades to prospect status if needed.
  • Lead file edited but DB stale? Click Rebuild dossiers + Queue T1 drafts on the home page (or use --refresh-queued).
8.

Graduate converted leads

When a reply turns into a real conversation, move the lead from discovery-vault/leads/ to discovery-vault/companies/ with the appropriate pipeline stage. Update discovery-vault/pipeline.md. The dashboard auto-pauses sends once the prospect's status is replied or paused, so no DB action needed.

Known gaps + things to watch

External tools (rare use)

See home for at-a-glance status, funnel coverage, and per-PMC engagement. See /templates to edit vault templates from the dashboard. See /dossiers to curate first-batch selection.