git:20260927.1dff839 to git:20260927.31de767

7 added, 4 removed. Audit A to A.

---
name: sales-outbound
description: >-
Turn ONE person's interest signal into a tracked CRM lead AND a ready-to-send
reply. The signal can be a copied DM / chat thread (Threads, IG, LINE, FB
Messenger, WhatsApp, email), a single row from an event feedback or
registration form, a website enquiry, or the text off a business card —
anything where one named person showed interest and left an email address.
Optionally hand it their profile link too. It does two things in one pass:
(1) collects who they are — parsing the signal and enriching from any
social/profile URL — and writes the client context into Zynkr CRM
(find-or-create company + contact, a new deal, a verbatim source note, and a
follow-up task for the next step); (2) drafts the outbound reply as a Gmail
DRAFT in Peter's inbox (never sends), in the signal's language and Peter's
voice, and — whenever the ask is a meeting — reads Peter's calendar FIRST and
offers three real slots in 台北時間. Trigger on /sales-outbound or when Peter
pastes a DM, a feedback-form row, or an enquiry and says "draft a reply",
"draft an outbound mail", "回這個", "幫我擬回覆", "把這個 lead 建進 CRM 並擬信",
"log this lead and draft the email", "turn this DM into a draft", "這個回饋幫我
建客戶並擬信", or otherwise hands over one person's interest signal and wants
both the CRM record and the reply prepared. Distinct from
sales-client-sourcing (BATCH-enriches a WHOLE survey Sheet into new columns —
no CRM records, no email), consult-intake (a weekly BATCH sweep of inbound
website consult EMAILS) and consult-project-specialist (documents ONE
sales/consulting MEETING transcript into a project + Drive folder + deal): this
one takes ONE person's signal and produces a lead record + a drafted reply, no
Drive folder, no meeting parsing.
Mode B — a five-touch outbound SEQUENCE for someone who has NEVER replied:
trigger on "一週五封", "連續開發信", "跑一整套序列", "同一個人五次", "五封序列",
or when Peter points at the 陌開名單 roster and wants a batch worked end to end.
Mode B claims the lead in the roster, reads Peter's calendar for real slots, picks a
role angle, writes all five emails, saves them as Gmail drafts, and logs the touch.
See references/sequence-5touch.md.
category: sales-consultant
project: sales-outbound
platform: claude
status: Done
author: Peter Tu
sheetId: "2.09"
input: "ONE person's interest signal carrying an email — a pasted DM/chat thread, an event feedback or form row, a website enquiry, or business-card text; optionally their profile URL."
process: "Parse the signal → enrich from any profile link → write the CRM lead (find-or-create company + contact, new deal, source note, follow-up task) → if a meeting is the ask, read the calendar for three 台北時間 slots → draft the Gmail reply → report."
output: "A CRM lead (company + contact + deal + note + task) in Zynkr CRM, plus a Gmail draft in Peter's inbox — with three calendar-checked 台北時間 slots when a meeting is the ask."
synergy: []
house-style: bound
---
# Sales Outbound
```bash
npx skills add https://github.com/peter-tu-zynkr/zynkr-skill-builder --skill sales-outbound
```
Interest reaches Peter from everywhere — a Threads/IG/LINE/FB DM, an event feedback
form, a website enquiry, a card handed over after a talk. Whenever one of those names
a person, carries an email, and shows what they want, Peter wants two things to happen
at once: the person should become a **tracked lead in the CRM**, and a **reply should
be sitting in his inbox as a draft**, ready to tweak and send.
This skill does both from a single paste. He drops the signal in (and, if handy,
the person's profile link), and it:
1. **Collects the client context** — parses the signal for name, the email they
gave, what they want, and who initiated; enriches role / company /
industry from any profile URL.
2. **Writes it into the CRM** — find-or-create company + contact, a **new deal**, a
**verbatim source note**, and a **follow-up task** for the next step.
3. **Drafts the reply** — a Gmail **draft** (never sent), in the signal's
language and Peter's voice, that moves the next step forward — with three
calendar-checked slots when that next step is a meeting.
**What counts as a signal** — any ONE of these, for ONE named person:
a copied DM / chat thread · a row from an event feedback or registration form ·
a website enquiry · the text off a business card · a forwarded email. What matters
is that it names a person, carries an email, and shows what they want. It is NOT a
whole spreadsheet of people (that is `sales-client-sourcing`) and NOT a meeting
transcript (that is `sales-follow-up` or `consult-project-specialist`).
It runs **autonomously**: parse → enrich → write CRM → check calendar → draft email
→ report. No mid-run confirmation — the email lands as a *draft* (safe, Peter reviews
before sending) and the CRM write is one atomic statement.
## Two modes
| | 前提 | 輸入 | 產出 |
|---|---|---|---|
| **Mode A** | 對方**已經回過話**,談到 demo/通話 | 一段貼上的 DM/對話 | 1 個 lead + 1 封回覆草稿 |
| **Mode B** | 對方**完全沒講過話** | 陌開名單裡的一個人 | 認領 + 5 封序列草稿 + 接觸紀錄 |
**Mode B 的完整做法在 `references/sequence-5touch.md`**,走它之前先讀那份。
不要把 Mode A 的推論預設(`stage=qualified`、`lifecycle=sql`)套到 Mode B——
那些是給「已經有回應」的人用的;Mode B 的人一律 `還沒接觸過 → 已聯絡,待回覆`。
Mode B 的寫入端**不是平台 CRM**:認領走陌開名單,接觸紀錄寫 `Outbound touches` 試算表。
這是設計,不是權宜——這批人還沒進 CRM,進了反而汙染 pipeline 數字。
Gmail 草稿則**看環境**:有可用的 Gmail MCP 就用,沒有就走 Playwright 網頁版
(reference 裡的選擇器與地雷是後者實測出來的)。走之前先確認這台機器有哪些工具。
## How this differs from its neighbours
- **sales-client-sourcing** — takes a WHOLE event/workshop survey **Sheet** and
batch-enriches it into new columns (官網 · 公司背景 · 陌生開發策略 · Hot Lead).
It writes back to the Sheet only — no CRM records, no email. Natural pairing:
run it to triage the sheet, then run THIS skill on each hot row.
- **sales-specialist** — the **business-card** pipeline (OCR a card image → contact
+ company + 名片 note + follow-up draft). Same shape, different input: use it when
the signal arrives as a photographed card rather than as text.
- **consult-intake** — a weekly BATCH job that sweeps inbound `website@zynkr.ai`
consult emails and de-dupes them into deals. Use it for the inbox sweep, not a
single pasted signal.
- **consult-project-specialist** — takes ONE sales/consulting **meeting transcript**
and documents it as a project: Weekly Update + numbered Drive folder + kickoff
Doc + deal. Heavy. Use it after a real meeting.
- **sales-outbound** (this one) — takes ONE person's **interest signal**
(pre-meeting) and produces a lead record + a drafted reply. No Drive folder, no
meeting parsing, no Weekly Update.
## Fixed facts (don't re-derive these)
- **Supabase project_id**: `uomieoqlkazknjgmfdda` (the shared Zynkr project; CRM tables are `crm_*`)
- - **Google account** for all Gmail tools: `peter_tu@zynkr.ai`
+ - **Google account** for all Gmail tools: `<your-google-workspace-account>`
- **Writing style is NOT owned by this file.** The house voice lives in two Google Docs
under `[@] 寫作指南` (`12DBdFz3SK22ie9im_ThFMI7IBRXsTZsV`), read at runtime with
`get_doc_as_markdown` (no tab parameter — use only `# 最終產出`, ignore `# 指令工程`):
《[2.2] 內文風格指南》 `1ect0fDoHZQ7srFEQvLNCSLsQk-UTawvbxpt3SteYP1M` **Part 3 §八 業務信件**
(positive rules) and 《[3.2] 禁用詞清單》 `1N5sHLP4qzmmhpCGsi6KElxi1z0MFe4QZ0Q_35T10Uyg`
**category X** (sales-email forbidden patterns). The Doc wins over the rules restated below.
- **CRM record URLs** for the report: `https://platform.zynkr.ai/deals/{id}` · `.../contacts/{id}` · `.../companies/{id}` — `{id}` is the uuid the SQL returns.
- Owner (`Peter Tu`), the caller's `workspace_id`, and the default pipeline (`銷售流程`) are resolved **live inside the SQL** — never hardcode their ids.
- **Do NOT log the drafted email as a CRM activity.** Peter's Gmail is already synced into the CRM (`app/lib/integrations/sync.ts`, 15-min cron): the moment he sends the draft, it auto-appears on the contact's 電子郵件 timeline as an outbound email. Logging it here would duplicate that row.
---
## Workflow
### 1 · Acquire + parse the signal
The signal usually arrives as **pasted text**. It may also be a pointer Peter expects
you to go read yourself — a Google Sheet URL plus a name ("the feedback from Rebecca"),
or a Gmail thread. Go fetch it rather than asking him to paste it again. Also capture
anything he hands alongside:
- A **profile URL** (Threads / IG / LinkedIn / FB / a website) → for enrichment in step 2.
- A **CRM record URL** (`…/contacts/{id}` or `…/companies/{id}`) → pass that uuid so
the deal links to the existing record instead of creating a duplicate.
Pull these fields (infer logically, never fabricate — leave blank if absent):
- **Name / handle** of the counterparty.
- **Email** they gave (the reply will go here; it's also the find-or-create key).
- **Phone**, when the source carries one (forms usually do).
- **What they want** — a demo, a call, "send me info", joining a beta, buying. On a
DM this is what they *agreed to*; on a feedback row it is what they *asked for*,
which is softer — see *Inference defaults*.
- **The next step** — becomes the follow-up task and the email's ask.
- **Who initiated** — Peter reached out (outbound) vs they came to him (inbound) → `lead_source`.
- **Language** of the signal (zh-TW vs EN) → the draft matches it.
- **The single most quotable line they wrote**, verbatim — the draft opens by
mirroring it, and the CRM note stores it unedited.
**Per-shape notes**
- **Feedback / registration form row** — read the header row too, so each answer is
attributed to the right question; keep the free-text answers verbatim, including the
"what else would you like to learn" style column, which is usually the real buying
signal. Note which event they attended → that is the `lead_source`.
- **Business-card text** — if it is still an image, use `sales-specialist` instead.
- **Website enquiry** — if it is a whole inbox's worth, use `consult-intake` instead.
### 2 · Enrich from the profile link
If a profile URL (or a resolvable handle) is present, fetch it with `WebFetch` and pull:
display name, bio/role, company, industry, follower count, any public contact email
or links. Fold this into the contact's `title` + the company record + the note. If no
link is given, skip — don't guess a bio.
### 3 · Build the client card
Assemble a compact summary and keep it for the report:
```
客戶名片
- 姓名 / 帳號 · …
- Email · …
- 身分 / 公司 · … ← from enrichment
- 來源 · Threads DM(Peter 主動 / 對方來訊)
- 對話結論 · 同意看 30 分鐘 demo
- 下一步 · 約時間、寄會議連結
- 語言 · zh-TW
```
### 4 · Write the CRM lead
Read `references/lead-insert.sql`, fill the placeholders, run it via
- `mcp__supabase__execute_sql(project_id="uomieoqlkazknjgmfdda", query=...)`. One
+ `mcp__supabase__execute_sql(project_id="uomieoqlkazknjgmfdda", query=...)`.
+ **Fill `<your-company-contact-email>` (the CRM owner the lead is filed under) first.
+ If you cannot resolve it, stop and ask for it — never run the insert with the blank
+ still in it.** An unfilled owner fails with a NOT NULL error (usually on `workspace_id`). One
statement:
- resolves owner + workspace + default pipeline live,
- **find-or-creates the company** (case-insensitive by name; skipped if no company),
- **find-or-creates the contact** (case-insensitive by email; reused if it already exists),
- inserts a **new deal**,
- logs a **`note`** activity (the verbatim conversation + who they are) and a **`task`**
activity (the agreed next step, due in `{{TASK_DUE_DAYS}}` days, assigned to Peter).
It returns `company_id`, `contact_id`, `deal_id`, `note_id`, `task_id`, plus
`contact_existed` / `company_existed` booleans so you can say so in the report.
**Escaping:** every `{{…}}` placeholder is replaced with a **SQL literal** — quote
text and double any single quote (`O'Brien` → `'O''Brien'`), or write `NULL`. For an
absent company, set `{{COMPANY_NAME}}` to an empty string `''` (the SQL then creates
no company and the deal's `company_id` is NULL).
**If the Supabase MCP is not authenticated** — you can tell because only
`mcp__supabase__authenticate` / `complete_authentication` are exposed and
`execute_sql` is missing — fall back to the `zynkr` MCP CRUD tools instead of
blocking: `list_contacts(search=<email>)` to dedupe, then `create_contact` →
`create_deal` → `create_note` → `create_task`. Each is a two-call handshake
(`confirm:false` to preview, then the identical call with `confirm:true`). They
resolve workspace + pipeline natively, but two things the SQL did for free you must
now pass explicitly: **`legal_basis`** (`legitimate_interest` — the same house value
baked into the SQL) and **`owner_id`** (Peter's workspace member id, from `whoami`).
There is no find-or-create on this path, hence the explicit dedupe first.
See *Inference defaults* below for the enum fields.
### 5 · If the ask is a meeting, derive three REAL slots
**Never invent availability.** Whenever the draft will propose a call / demo / meeting,
read the calendar first — a guessed window is usually already booked.
1. **Read the events.** Use
`mcp__claude_ai_Google_Calendar__list_events(startTime=…, endTime=…, timeZone="Asia/Taipei", orderBy="startTime")`
across the next 5–10 working days.
⚠️ Do **NOT** use `mcp__google-workspace__get_events` — the Calendar API is disabled
on Peter's GCP project (`963483219986`) and it returns a hard error.
2. **Find the bookable windows.** Peter's calendar marks them with events literally
titled **`Available`** (and **`Not available`** for the inverse). Take each
`Available` block and subtract every real meeting overlapping it. A leftover
shorter than 30 minutes is not a slot.
3. **Pick three**, spread across different days. Express each as the whole remaining
window (`15:30–18:00`), not one fixed time — let the prospect choose inside it.
4. **Convert to Taipei and say so.** Peter's calendar is `Europe/Amsterdam`; almost
every prospect is in Taiwan (+886), 6–7 hours ahead, so an unlabelled 「下午」 means
two different things. Label the block **「(台北時間)」**. Keep the Amsterdam
equivalents for the CRM note and your report — not for the email.
5. **Record them on the deal** so Peter needn't re-convert when she replies.
If the ask is not a meeting (send info, share a link), skip this step entirely.
### 6 · Draft the reply (Gmail draft — never send)
Draft the outbound reply with
- `mcp__google-workspace__draft_gmail_message(user_google_email="peter_tu@zynkr.ai", to="<their email>", subject="…", body="…")`.
+ `mcp__google-workspace__draft_gmail_message(user_google_email="<your-google-workspace-account>", to="<their email>", subject="…", body="…")`.
To revise a draft you already made this run, use
`mcp__claude_ai_Gmail__update_draft(draftId=…)` — it edits in place, so Peter is not
left with two near-identical drafts to choose between.
Rules for the draft:
- **Match the signal's language** (zh-TW reply to a zh-TW signal, EN to EN).
- **Peter's voice** — warm, concise, peer-to-peer; mirror the tone of the source (if
the DM was casual and friendly, so is the email). No corporate boilerplate.
- **Open by mirroring their own words** — quote back the concrete thing they said they
wanted. On a feedback row, that is the verbatim free-text answer. This is what makes
the mail land as a reply rather than a pitch.
- **Earn the meeting with a point of view** — one short paragraph of substance (how
Peter would approach their problem, what order he'd do it in) before the ask. Never
jump straight from greeting to calendar.
- **Move the next step forward** — if a meeting is the ask, offer the three slots from
step 5; if "send info", point to it.
- **Concrete subject line**, naming their problem in their words.
- **House style is owned by 《[2.2] 內文風格指南》 Part 3 §八 and 《[3.2] 禁用詞清單》 category X**
(see *Fixed facts*) — read them; the Doc wins over this summary. The load-bearing few:
- Greeting `Hi <名>,` / `哈囉 <名>,` / `<名> 你好,` / `<名> 您好,` (cold, HR, senior), full-width
comma. 你/您 follows the counterpart and only ever steps **down**, never back up.
- 我們 for company commitments, 我 for what Peter does himself — mixing is deliberate.
- A standard outbound mail runs **280–400 字**, blank-line paragraphs of 1–3 sentences.
- Numbered lists are plain **`1. 2. 3.`** — NOT 1️⃣2️⃣3️⃣, never circled ①②③. Emoji numerals are
a *UI* convention and are banned in email by 禁用詞清單 S and X.
- At most **two lists in the whole mail**: the three things to discuss, and the slot block.
Everything else is prose — a bullet wall is the 項目符號牆 category X forbids.
- The dash is the **half-width ` — ` with one space each side**. Never the full-width ——.
- Headings/taglines take **no 句號 (。)**; `·` for series separators; no Markdown in the body.
- Sign off `Best regards,` + `Peter`. No signature block.
- **The slot block is fixed house wording.** Three slots, a fourth escape line, then
the invite promise — as Peter hand-edited it on 2026-09-27 (it replaced
「以下時間我都可以,再麻煩你挑一個(台北時間):」 and 「4. 其他你方便的時間」):
```
不曉得以下有沒有方便的時間(台北時間)?
1. 9/8(二)20:00–22:00
2. 9/9(三)15:30–18:00
3. 9/10(四)15:00–16:00
4. 或其他你方便的時間
確認後我會寄出會議邀請。
```
- **Sign off** `Best regards,` then `Peter`.
- **Create a draft, never send.** Peter reviews and sends himself.
- **Verify before reporting** — confirm the draft carries label `DRAFT` and that
`in:sent to:<their email>` returns nothing. Say so in the report.
### 7 · Report
Show the client card, then a compact artifact table:
```
- 已建立 lead 並擬好回信草稿:超哥(行銷超哥 Chao.Marketing)
+ 已建立 lead 並擬好回信草稿:王小明(範例科技 ExampleCo)
| 產出 | 連結 / 狀態 |
|------|------------|
| CRM Deal | <deal url> |
| 聯絡人 | <contact url>(新建 / 既有) |
| 公司 | <company url>(新建 / 既有) |
| 活動 | note · 1 task |
| Gmail 草稿 | 已放進收件匣,待你過目後寄出(已確認未寄出) |
```
When the mail carries slots, also show them with their Amsterdam equivalents and the
`Available` block each came from — that is what lets Peter sanity-check you did read
the calendar rather than guess.
If the contact already existed, say so (you attached a new deal to the existing
person rather than duplicating them).
---
## Inference defaults (Peter overrides by just saying so)
Pick the closest enum from the conversation; use the fallback when there's no signal.
- **stage** (`new|contacted|qualified|proposal|won|lost`) — they engaged and **agreed
to a demo / call / clear next step** → **`qualified`**; softer "sounds interesting,
maybe" → `contacted`; bare first reply → `new`. A **feedback / form row is `new`** —
they described a need, but nothing has been agreed and Peter has not replied yet.
- **lead_source** (`content|workshop|referral|outbound|other`) — **Peter messaged them
first** → **`outbound`**; they came via a post/content → `content`; **they name an
event or livestream they attended → `workshop`**; warm intro → `referral`; unclear
→ `other`.
- **contact lifecycle_stage** (`subscriber|lead|mql|sql|opportunity|customer|…`) —
demo/call agreed → **`sql`**; **attended something and self-described a need →
`mql`**; otherwise **`lead`**.
- **company** — a personal gmail/yahoo address with no employer named means **no
company record**. Leave it null and note it; guessing an employer poisons the CRM.
Create it after the first call, when they've told you.
- **priority** (`low|medium|high`) — default **`medium`**; eager / near-term → `high`.
- **deal_type** — **`new_business`** (this is a fresh lead).
- **value** — **`NULL`** unless a concrete figure was discussed (beta / early chats have none).
- **close_date** — `NULL` unless a date was stated.
- **task_due_days** — default **3** (chase the demo/call within a few days).
## Why it's built this way
- **One paste → both outputs.** The point is to kill the two-step (log the lead, then
go write the email). Peter pastes once; the lead is tracked and the reply is waiting.
- **Email is a draft, not a send.** Outbound to a real prospect is Peter's call —
the skill prepares, he approves. That's also why it needn't confirm before running.
- **Find-or-create, always-new deal.** A person can come back for a second deal, but
they shouldn't be duplicated as a contact — so contact/company are keyed
(email / name), while each run books a fresh deal (re-running = a deliberate redo).
- **Don't log the email activity.** The Gmail→CRM sync already captures it on send —
logging here would double the row. The skill writes the *note* (the source signal,
which the sync can't see) and the *task*, and leaves the email to the sync.
- **Slots come from the calendar, never from memory.** Peter sits in Amsterdam and
nearly every prospect sits in Taipei, six hours ahead — so an invented "next Tuesday
afternoon" is both probably booked and ambiguous about whose afternoon it means.
Both failures cost a round-trip and read as careless. Three real windows plus an
escape line converts in a single reply.
- **One signal shape in, one shape out.** The input parser is the only thing that
differs between a DM, a feedback row and a card — everything downstream (CRM write,
calendar, draft, report) is identical. That is why this skill absorbed the extra
input types instead of a near-duplicate skill being cloned for each one.
## House style
Writing style is **not owned by this file**. The house voice lives in two Google Docs under
`[@] 寫作指南` (`12DBdFz3SK22ie9im_ThFMI7IBRXsTZsV`), read at runtime:
- 《[2.0] Zynkr 通用風格指南 House Voice》 `10bOIQwRm9Pxwgct4hlwCwK_B4Pipai1HqBPZKzyRHSE` —
the universal core, plus the addendum for this surface
- 《[3.2] 禁用詞清單 Forbidden Words》 `1N5sHLP4qzmmhpCGsi6KElxi1z0MFe4QZ0Q_35T10Uyg`
Read both before producing client- or reader-facing text, and scan the draft against 《[3.2]》
before handing it over. If Drive is unreachable, say so in the output rather than proceeding
unchecked. Never re-implement either list inside this file.
---
## Mode B · 五封序列
完整步驟、職位切角表、外洩防線、實測地雷全在 **`references/sequence-5touch.md`**。
這裡只放不能忘的五條:
1. **時段一律取自 Peter 日曆名為 `Available` 的事件**,並扣掉壓在上面的既有會議。沒標就問人,不要編。
2. **階梯是 ask → ask → 長相 → 純給 → 放手。** 第 4 封必須零 ask,否則第 5 封還沒寄就被封鎖。
3. **`[2.7] 政府補助 knowledge` 絕對不可外發**(它自己第 5 節標了「內部資訊,勿外流」)。
顧問姓名、拆帳、加價機制、盤點數字、難度分級一律不得進信裡。
4. **草稿建好 ≠ 已寄出。** 接觸紀錄的 Remark 要註明「尚未寄出」,寄出後才拿掉。
(2026-08-31 有三封被記成「已聯絡」卻躺在草稿匣,別再犯。)
5. **五封就停。** 沒回的改 `暫時沒需求`。一天一批、一批不超過 5 人。
> 這批人是付過錢、上過 9–17 場的學員,`zynkr.ai` 網域信譽與《彼得的外商隨筆》電子報共用。
> 被檢舉成垃圾郵件是整個網域一起遭殃,不是這一封的事。