Oulang Continuous Outreach Skill
Default identity
Unless the user overrides, this skill operates the Oulang.ai pre-seed €500K–€1M raise and the Spanish-Chinese diaspora partnership track. Sender identity:
- From:
- Sender name: Sami Halawa — Founder, Oulang.ai (欧浪网)
- Legal: AGENTS AI Ltd (UK 16570822)
- WhatsApp signature: +34 679 794 037
- Deck link:
https://oulang.ai/investors
- Honest MRR framing: "€3.9K cash MRR (excludes admin credit grants)"
- Real moat: WeChat group-of-groups lock-in (NOT "Mandarin language")
If the user provides a different campaign context, override these but keep the same loop.
Infra (canonical, do not improvise)
| Resource | Address | Notes |
|---|
| Notion DB (Investor Outreach Tracker) | collection://24dc90d8-2308-4c5a-88da-348926d78370
| URL: https://www.notion.so/pime/6705b564c5d34f55a43fce064f77f60b |
| Direct Gmail SMTP | | Use , , , ; no relay/proxy service |
| Exa Websets API | https://api.exa.ai/websets/v0/websets
| Use existing websets when possible, create new ones with refined queries |
Sender SMTP contract:
,
,
, and
must be supplied from the current run environment or user-provided secret context. Do not hardcode the app password in the skill, logs, drafts, commits, or summaries.
The self-correcting loop (run every cycle)
1. AUDIT → 2. ANALYZE → 3. REFINE → 4. DISCOVER → 5. WRITE → 6. SEND → 7. RECORD
↺ loop back to 1
1. AUDIT — what happened to the last batch?
Before drafting a single new email, query Notion for everything in
since the last audit and compute:
- Reply rate, bounce rate, interested rate, passed rate, silent rate.
- Per-segment breakdown (Tier 1 VC vs Tier 2 vs Chinese operator vs chamber vs angel).
- Per-hook performance (which subject-line hooks got opens/replies).
- Bounce reasons — was the address guessed? from Exa? form-only? domain-locked?
Then, broad-keyword IMAP/Gmail search for all prior sent mail, replies, bounces, blocks, drafts, archived mail, spam, trash, and outbox evidence matching campaign topic, person, organization, domain, email, phone, form URL, and aliases — NOT sender-filtered, because real replies often come from a different address than the one we sent to (assistants, partners, alternate inboxes). Search across all known Sami identities:
This is a hard rule. Past runs missed real replies (EurochinaBridge warm-lead almost lost) because of sender-filtered search.
For every potential outbound target, read the full content of every matching sent message, reply, bounce, block notice, form confirmation, draft, and forwarded thread before deciding whether to contact. Snippets, subject lines, tracker status, and search counts are not enough. If a mailbox/tool only exposes snippets, mark the target blocked until a full thread/body read is available or use another authorized mailbox path.
2. ANALYZE — what went wrong, what went right?
For each failure mode, record root cause in the Notion page Notes column:
- Bounced / blocked / invalid: first classify whether the row is legacy/pre-Exa or Exa-sourced. Most wrong-email rows are legacy/pre-Exa rows from the first outreach rounds, not Exa Websets output. Remove invalid legacy emails from the send queue immediately and re-find/re-enrich the target through Exa Websets instead of guessing a replacement.
- Silent (no reply, no bounce): was it spam-foldered (identical subject lines across batch)? was hook generic? was ask too aggressive (€500K solo-runway framing)? wrong recipient seniority?
- Polite pass: was thesis genuinely off? was stage off? note for de-prioritization in next wave.
- Replied / interested: which hook resonated? log into a "what works" reference pattern that future drafts must reuse.
Past systemic failures this skill must guard against:
| Pattern | Failure | Fix |
|---|
| 18 of 25 emails identical subject line | Spam-folder signal across VC inboxes | Each subject must be unique + tied to recent investment of recipient |
| "I saw your recent activity" generic opener (14×) | Read as mail-merge | Cite specific named portfolio company + date |
| "€500K / 12-month runway" | Implies solo-salary burn, signals "you can't lead" | Reframe to "€500K–€1M, any lead welcome" |
| "+34 — (will share on reply)" | Amateur hide | Always include WhatsApp +34 679 794 037 in signature |
| Promised deck/Loom with no link | Dead promise | Always include |
| €3,952 MRR exact number with no caveat | VCs grep, find admin-credit subsidy, lose trust | Disclose: "€3.9K cash MRR, excludes admin credit grants" |
| 5 Chinese emails identical body | Insulting in Chinese business etiquette | Each must reference target's specific company/role |
| Chamber emails with VC language | Wrong genre, instant ignore | Strip fundraising entirely, lead with cross-promo offer |
| Sender-filtered IMAP search for replies | Misses real replies from assistants/alternate addresses | Broad-keyword search across all 7 inboxes |
| Diagnostic test sends going to real recipients | Embarrassing duplicate received | NEVER send a smoke test to a real recipient; use self-loop |
3. REFINE — update Exa criteria and email hooks
Take the analysis output and rewrite the next Exa Webset query to remove patterns that produced bounces or no-replies, and double down on patterns that produced engagement.
Examples of refinement that came out of past runs:
- Removed from Exa criteria after v4: "Pre-seed VCs Spain" alone → too noisy. Added: "with marketplace OR diaspora investments in last 12 months".
- Added after v5: "Wallapop LP", "Marshmallow LP", "diaspora SaaS LP" — investors who already bet on similar pattern.
- Removed: investors with , , — they don't read cold email and route through Twitter-DM only.
- Added bonus criteria: "publicly tweeted about Spain market in last 90 days" or "wrote LP thesis on Mandarin commerce".
- For partnership track: "bought ads in Mandarin Spanish media in last 6 months" (real signal of intent to reach our users).
Each new Webset query should be saved with an
, the hypothesis, and a kill criterion.
4. DISCOVER — mine websets, dedupe against Notion
Query existing Exa Websets first. Only create new ones if existing data is exhausted or the refinement requires a brand-new criteria. Always inspect the webset query/criteria, item status, enrichment fields, and source URLs before importing. Prefer current completed Websets with explicit public-contact enrichment; do not use stale manual rows or guessed emails as source truth.
Legacy/pre-Exa rows are lower trust. If a non-Exa row has a bounced, blocked, guessed, stale, or unverifiable email, remove it from
and re-run Exa Websets for that organization/person/domain. Do not spend time hand-repairing guessed addresses unless Exa or another public source verifies the exact email/channel.
GET https://api.exa.ai/websets/v0/websets/{webset_id}/items
For each enrichment item, build a row only when it is send-ready or useful as a verified channel target. Do not create weak placeholder rows during "find more" automation.
{
Organization: <company>,
Contact: <person name>,
Verified Public Email: <best email — see Email Selection rules below>,
Public WhatsApp Phone: <phone>,
Contact Source URL: <linkedin or homepage>,
Priority Tier: Tier 1 / Tier 2 / Tier 3 / Weak,
Priority Rank: <1-100 numeric>,
Contact Enrichment Status: "Exa found",
Contact Enrichment Notes: <Portfolio | 2026 Activity | Thesis Match | Sector | City | Check Size | Inbound Channel>,
Notes: <experiment ID + hypothesis>,
Draft Status: "Drafted",
Reply Status: "🚀 TO SEND"
}
If a candidate lacks a verified public email, public WhatsApp number, or explicit public pitch/contact form, skip it and log the reason in the cycle output instead of adding a placeholder row. The tracker should stay action-oriented: rows are for people/channels that can be contacted after the prior-communication audit, not for generic research backlog.
Communication gate and channel selection rules
Before any outbound communication (email, WhatsApp, form, LinkedIn, DM, or contact-page message), verify prior communication history first. This is not only an email safety check.
Required evidence before communicating:
- Search the tracker/Notion row and nearby duplicate rows by organization, person, email, WhatsApp phone, form URL, and normalized domain.
- Search all available inbox, sent, archived, spam, trash, draft, and outbox folders for the same organization, person, email, WhatsApp phone, form URL, and domain.
- Read the full content/body of the relevant sent messages, replies, bounces, block notices, drafts, and forwarded threads before choosing the channel. Do not rely on row status, snippets, or subject lines when mail or WhatsApp evidence disagrees.
- If prior communication is found, continue in that same thread/channel unless the reply explicitly routes to a different channel.
- If a WhatsApp number is available and there is no existing WhatsApp reply/thread that changes the plan, prefer WhatsApp as the highest-priority live channel.
- Email remains the default channel when there is verified email and no stronger WhatsApp/form instruction.
- Form/Typeform/Airtable/contact-page submission is used only when the recipient or public process explicitly says pitching/replies must go there, or when a reply asks for the form route.
- If the evidence is contradictory or incomplete, do not communicate. Do not create a new placeholder row from discovery. If the row already exists, update its notes/status with the exact conflict and leave it out of the send batch.
- If an email is bounced, blocked, syntactically invalid, guessed, stale, or not publicly verified, remove it from immediately. Update the row to
Reply Status = 🔧 FIX EMAIL
and clear the send path, or delete/archive the row when the active tracker tooling supports that. Never leave invalid emails as send-ready.
- For legacy/pre-Exa invalid rows, the next action is "refind via Exa Websets", not "manual guess". Add a short note such as
Legacy/pre-Exa invalid email removed; refind via Exa Websets required
.
Email selection rules (after the communication gate)
- Personal email if the person publicly publishes one ("personal email: jane@fund.com" in Exa enrichment) → use it.
- , , generic inboxes → use them, NOT the partner's personal email, when public preference says so. Cherry, Mangrove, Ada all do this.
- Form / Typeform / Airtable submission → only after the communication gate proves the form is the requested pitching/reply channel. Examples: Point Nine typeform, Seedcamp typeform, Bethnal Green Airtable, Ada Ventures form, Blue Lake Airtable, Systemiq contact form.
- Email guessed by pattern (firstname.lastname@domain) → red flag. Skip the candidate unless another source verifies the address publicly. Do NOT send guessed-pattern emails.
Dedupe rules
Before adding a new row, fuzzy-match against existing Notion rows by:
- exact
- normalized name (lowercase, strip punctuation)
- if both present
If match found, MERGE enrichment notes onto the existing row instead of creating a duplicate. Past runs created multiple duplicate rows (e.g. Point Nine, Passion, Ganas all have ≥2 rows in the tracker — this is technical debt to clean up).
5. WRITE — per-target personalized drafts
Apply the mandatory structure for every email (no exceptions, no template-batching):
Subject: <unique per-target hook tied to recipient's named recent investment or public signal>
Hi <FirstName>,
<Hook paragraph: name a specific portfolio company / thesis / 2026 announcement, explain in ONE sentence why their pattern matches Oulang>
<Status block — bullet form>:
- 40K MAU, 13K registered users (40% growth QoQ)
- €3.9K cash MRR March 2026 (excludes admin credit grants — real paid subs)
- Mandarin-graph distribution: WeChat-native onboarding, 0€ paid acquisition
- Moat = WeChat group-of-groups lock-in, not language (groups don't port)
<One-sentence "why you specifically" — different from the hook>.
Open to €500K–€1M round, any lead check welcome. 15-min call if there's a fit, or I send the data room and you decide.
Best,
Sami Halawa — Founder, Oulang.ai (欧浪网)
AGENTS AI Ltd (UK 16570822) · Valencia/Madrid
oulang.ai · sami@oulang.ai · WhatsApp +34 679 794 037
Deck + metrics: https://oulang.ai/investors
For Chinese-language emails: same structure, but each greeting
and the company-specific cross-promo proposal MUST differ per target. Reference target's actual company, role, and at least one concrete partnership idea (e.g., "您餐厅在欧浪网美食板块独家露出" for a restaurant).
For chamber/institutional emails: STRIP all fundraising language. Lead with cross-promo offer (Mandarin translation, event co-host, member directory swap).
Self-critique gate (run BEFORE send)
For each draft, internally answer:
- Is the subject unique across this batch? → If no, rewrite.
- Is the hook specific (named portfolio/announcement)? → If no, rewrite.
- Is the ask reframed away from "€500K solo runway"? → If no, fix.
- Is the deck link present? → If no, add.
- Is MRR framed honestly (cash, excludes credits)? → If no, fix.
- Is recipient seniority + stage actually correct? → If wrong, SKIP not send.
- Does this email overlap with another in the batch on hook? → If yes, vary.
A draft that fails any gate must be rewritten or dropped, NOT sent.
6. SEND — fire directly via Gmail SMTP
python
import os, smtplib, ssl, time
from email.message import EmailMessage
smtp_user = os.environ["SMTP_USER"] # default: sami@oulang.ai
smtp_password = os.environ["SMTP_PASSWORD"]
smtp_host = os.environ.get("SMTP_HOST", "smtp.gmail.com")
smtp_port = int(os.environ.get("SMTP_PORT", "587"))
for i, d in enumerate(drafts):
msg = EmailMessage()
msg["From"] = smtp_user
msg["To"] = d["to"]
msg["Subject"] = d["subject"]
msg.set_content(d["body"])
with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
server.starttls(context=ssl.create_default_context())
server.login(smtp_user, smtp_password)
server.send_message(msg)
print(f"#{i+1} {d['to']:45} sent via direct SMTP")
time.sleep(1.5) # mandatory pacing
Run the sender with inline SMTP environment values, for example:
bash
SMTP_USER=sami@oulang.ai SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_PASSWORD="$SMTP_PASSWORD" python3 scripts/send_batch.py drafts.json
Only use direct Gmail SMTP for Oulang outreach. If direct Gmail SMTP fails, fix the SMTP credential/environment issue or stop the send batch; do not route around it.
Auto-dispatch rule:
The user has corrected the workflow: when the request includes send intent such as "send", "always send", "auto-send", "send the emails", "find and send", "next wave", or equivalent typo-heavy wording, treat that as explicit authorization for this cycle. Do not stop at adding rows. Complete steps 5, 6, and 7 for every candidate that passes all gates.
If the user only asks to "find/add to Notion" with no send intent, create send-ready rows only and leave them at
+
. Do not send in that narrower capture-only mode.
Critical send-time rules:
- Never send a "test" email to a real recipient. Self-loop to if you must probe.
- Dedupe at send time: a single email address never receives the same campaign twice in 24h.
- Batch in groups of 10 to survive sandbox tool-call timeouts.
- After every batch, log to
/mnt/user-data/outputs/<campaign>_send_log_<date>.txt
when writable; if that directory is read-only, use /tmp/oulang-outreach-<date>/
and mention the fallback path in the cycle summary.
- Only send candidates that have a verified public email or an explicit public channel selected by the communication gate.
- Skip unverified, contradictory, or incomplete candidates instead of parking them in the tracker.
7. RECORD — update Notion immediately
For every sent email:
Draft Status: Sent
Reply Status: No reply
Sent Date: <today, ISO>
Notes: <campaign tag> + per-target hook used + experiment ID
Use the Notion MCP
tool with
command: update_properties
. Loop one update per page, accepting ~1s per call.
After the batch, write a campaign summary doc to
:
<campaign>_send_log_<date>.txt
<campaign>_critique_<date>.md
<campaign>_drafts_<date>.json
These become inputs to the NEXT cycle's AUDIT step.
When to use which Notion field
The Investor Outreach Tracker has these columns. Stay consistent:
- (title) — official entity name
- — person name (full)
- — only verified or publicly published
- — free text for "form: typeform.com/x" or "linkedin DM only" cases
- — E.164 format
- — Tier 1 / Tier 2 / Tier 3 / Weak
- — numeric 1-100, lower = higher priority
- — Drafted / Reviewing / Ready / Sent
- — 🚀 TO SEND / 💤 WAITING / 📨 REPLY NOW / 🔧 FIX EMAIL / ✅ DONE
Contact Enrichment Status
— Existing in source / Exa found / Not found
- — long text with Portfolio / 2026 Activity / Thesis Match / Sector / City / Check Size / Inbound Channel pipe-separated
- — ISO date
- — LinkedIn or homepage
- — campaign tag + per-target hook + experiment ID + outcome notes
Campaign archetypes (pick one or define new)
The skill ships with these archetypes. The user can name them in the request:
- — EU pre-seed VCs with marketplace/diaspora thesis. Tier 1: Mangrove, Passion, InReach, Point Nine, Seedcamp, Cherry, Earlybird. Output: 25–40 personalized emails per wave.
- — Latina/MENA/Asian diaspora-focused funds and operators. Tier 1: Ganas (already passed), Portfolia, Diaspora Ventures.
- — Chinese-origin Spain-based angels, family offices, and SME owners. Sources: Cobo Calleja directory, La Vanguardia "Spanish-Chinese 100", Cámara de España member list.
- — Chambers, business associations, Confucius Institutes. ZERO fundraising language. Cross-promo only.
- — Partners who can drive Mandarin users to Oulang (WeChat KOLs, Spanish-Mandarin newsletter operators, university Chinese student associations).
- — Spanish-native businesses that want to reach Chinese-diaspora buyers. Lower-funnel partnership pitch.
Each archetype has its own set of Exa Webset templates. See
reference/webset_templates.md
.
Observability checklist (run end of each cycle)
Hard rules (do not violate)
- AUTO-SEND WHEN THE REQUEST SAYS TO SEND. Send intent includes "send", "always send", "auto-send", "send the emails", "find and send", "next wave", or equivalent typo-heavy wording. In that mode, do the audit, write personalized drafts, send verified recipients, update Notion, and log proof.
- DO NOT SEND IN CAPTURE-ONLY MODE. If the user only asks to find/add/store contacts, do not send.
- NEVER communicate without checking previous communication first. This applies to email, WhatsApp, forms, LinkedIn, DMs, contact pages, and any other outbound channel.
- NEVER treat a form as exempt from conflict checks. Forms can proceed without email-thread blocking only when the prior-communication audit proves the form is the correct channel.
- NEVER send test mail to real recipients. Self-loop to .
- NEVER use a send relay for Oulang outreach. Direct Gmail SMTP is canonical. A relay 500/503 is not a retry condition; it means the wrong send path was used.
- NEVER repeat a known systemic mistake. The mistakes table above is the full list as of 2026-04-26; if a new pattern appears, append to it.
- NEVER reuse a hook across recipients. Each subject + opening sentence must be unique.
- NEVER send unverified candidates. Verify email or channel first; otherwise skip and log the reason.
- NEVER skip the audit step. Every cycle must start with the prior wave's critique.
- NEVER turn this into general Oulang ops. This skill is only for finding leads, auditing prior communications, contacting verified targets, and updating the outreach tracker.
- NEVER claim a wave succeeded without log evidence. Every send writes a log file.
- NEVER decide from snippets. Prior-contact checks require full email/thread content for every matching sent, reply, bounce, block, draft, spam, trash, and forwarded thread.
- NEVER leave invalid emails queued. Bounced, blocked, guessed, stale, or unverifiable addresses must be removed from and marked or archived/deleted if the tracker permits.
- NEVER blame Exa for legacy guessed-email failures without evidence. First-round/pre-Exa rows are suspect by default; remove and re-find them through Exa Websets.
Reference files (read these alongside SKILL.md)
reference/webset_templates.md
— Exa Webset query templates per archetype
reference/email_templates.md
— Per-archetype email skeletons + signature blocks
reference/mistakes_log.md
— Append-only log of every mistake learned from + the fix
- — Hypothesis + result for each Exa Webset experiment
reference/oulang_state_snapshot.md
— Latest known state of the Oulang campaign (recipients, replies, pending forms, bounces) as of skill build
- — IMAP broad-keyword search across 7 Sami inboxes
- — Direct Gmail SMTP batch sender with pre-send safety gates
- — Notion property updater with dedupe check
- — Exa Websets miner with experiment tracking
- — Skill manifest for skill-CLI installer
Initial onboarding (first time used in a session)
When this skill is loaded for the first time in a session, read in order:
- This SKILL.md
reference/oulang_state_snapshot.md
— to ground in the current Oulang campaign state
reference/mistakes_log.md
— to load the do-not-repeat list
- The user's actual ask
Then proceed with the loop step that fits the ask:
- "audit / who replied" → step 1
- "next wave / fix the previous" → steps 1+2+3
- "send it" / "fire it" / "always send" / "send the emails" → steps 5+6+7 for existing send-ready rows
- "find more" with send intent → steps 1+2+3+4+5+6+7, skipping weak/unverified candidates instead of creating manual-review rows
- "find more" without send intent → steps 3+4 only, creating send-ready rows and skipping weak/unverified candidates
Definition of done
A wave is "done" only when:
- All drafts passed the self-critique gate.
- Every outbound communication target has current prior-communication evidence checked across tracker duplicates and available inbox/sent/spam/trash/draft/outbox folders, with full message bodies read for all matching sent/reply/bounce/block evidence.
- All send-intent drafts were sent OR explicitly skipped with the evidence that made sending unsafe; capture-only runs created only send-ready rows.
- Notion is updated row-by-row.
- Logs are in /mnt/user-data/outputs/.
- Critique doc is written for the NEXT cycle to consume.
- New Exa Webset experiment hypothesis is recorded.
Anything less is "in flight", not done.