$ cat post.metadata

Claude APIで営業パイプラインを自動化する -- GTMエンジニアのAI活用実践

GTMエンジニアリングAI自動化

GTMエンジニアがClaude APIを使って営業パイプラインを自動化する具体的な実装を解説する。リードスコアリング、企業リサーチ自動化、パーソナライズドメッセージ生成の3つのユースケースをTypeScriptコード付きで紹介。

$ cat post.content | render --format=markdown

Claude APIで営業パイプラインを自動化する -- GTMエンジニアのAI活用実践

この記事の位置づけ

前回の記事でGTMエンジニアリングの4層モデルを解説した。今回はそのLayer 2(AI処理層)にフォーカスし、Claude APIを使った営業パイプライン自動化の具体的な実装に踏み込む。

Vercel COOの事例で紹介した「SDR10人をAIエージェント+QA1人に置き換えた」戦略の技術的裏側を、動くTypeScriptコードで再現する。SLG組織でGTMエンジニアが実際に書くコードのイメージを掴んでほしい。

営業パイプライン自動化の3つのユースケース

自分が実際に手応えを感じた領域として、Claude APIで自動化した3つのユースケースを取り上げる。

ユースケース入力AI処理出力
1. リードスコアリング企業データ(業種・規模・技術スタック)ICP適合度を0-100で算出CRMにスコア書き込み + 営業通知
2. 商談前リサーチ自動化企業URL(IR/ニュース/採用ページ)構造化データに変換 + 課題仮説生成1ページサマリーをCRMに登録
3. パーソナライズドメッセージ生成企業情報 + 担当者情報課題に合わせた提案メッセージA/Bテスト可能な複数パターン

共通基盤: Hono APIサーバーとAnthropicクライアント

3つのユースケースを束ねるAPIサーバーをHonoで構築する。

typescript
// src/index.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import Anthropic from '@anthropic-ai/sdk' const app = new Hono() const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, }) export { app, anthropic }
typescript
// src/types.ts import { z } from 'zod' export const CompanySchema = z.object({ name: z.string(), domain: z.string().url(), industry: z.string(), employeeCount: z.number().int().positive(), annualRevenue: z.number().optional(), techStack: z.array(z.string()).default([]), recentNews: z.array(z.string()).default([]), irSummary: z.string().optional(), hiringRoles: z.array(z.string()).default([]), }) export const ContactSchema = z.object({ name: z.string(), email: z.string().email(), title: z.string(), department: z.string().optional(), linkedinUrl: z.string().url().optional(), }) export const LeadSchema = z.object({ company: CompanySchema, contact: ContactSchema, source: z.enum(['inbound', 'outbound', 'referral', 'event']), createdAt: z.string().datetime(), }) export type Company = z.infer<typeof CompanySchema> export type Contact = z.infer<typeof ContactSchema> export type Lead = z.infer<typeof LeadSchema>

ユースケース1: リードスコアリング

ICP(理想的顧客プロファイル)の定義

スコアリングの前に、自社にとっての理想的顧客を構造化する。これがプロンプトの核になる。

typescript
// src/scoring/icp.ts import { z } from 'zod' export const ICPSchema = z.object({ targetIndustries: z.array(z.string()), employeeRange: z.object({ min: z.number(), max: z.number(), }), techSignals: z.array(z.string()), budgetIndicators: z.array(z.string()), painPoints: z.array(z.string()), disqualifiers: z.array(z.string()), }) export type ICP = z.infer<typeof ICPSchema> // BtoB SaaS企業向けのICP定義例 export const defaultICP: ICP = { targetIndustries: [ 'SaaS', 'FinTech', 'EC/小売', '製造業(DX推進中)', 'メディア/広告', ], employeeRange: { min: 50, max: 2000 }, techSignals: [ 'HubSpot', 'Salesforce', 'Slack', 'AWS', 'GCP', 'React', 'TypeScript', ], budgetIndicators: [ '資金調達済み(シリーズA以降)', 'DX推進部門の存在', '年間IT予算1億円以上', '営業チーム10名以上', ], painPoints: [ '営業生産性の低さ', 'リードの質のばらつき', 'CRMのデータ品質問題', '手動レポーティングの工数', ], disqualifiers: [ '従業員10名未満のスタートアップ', '公共機関(調達プロセスが長い)', '競合他社', ], }

Claude APIによるスコアリング実装

typescript
// src/scoring/lead-scorer.ts import { z } from 'zod' import { anthropic } from '../index' import type { Company, Lead } from '../types' import type { ICP } from './icp' const ScoringResultSchema = z.object({ score: z.number().min(0).max(100), breakdown: z.object({ industryFit: z.number().min(0).max(25), sizeFit: z.number().min(0).max(25), techFit: z.number().min(0).max(25), intentSignals: z.number().min(0).max(25), }), reasoning: z.string(), suggestedAction: z.enum([ 'immediate_outreach', 'nurture_sequence', 'disqualified', 'needs_enrichment', ]), disqualifyReason: z.string().optional(), }) export type ScoringResult = z.infer<typeof ScoringResultSchema> export async function scoreLead( lead: Lead, icp: ICP ): Promise<ScoringResult> { const prompt = buildScoringPrompt(lead, icp) const response = await anthropic.messages.create({ model: 'claude-sonnet-4-5-20250514', max_tokens: 1024, messages: [{ role: 'user', content: prompt }], }) const text = response.content[0].type === 'text' ? response.content[0].text : '' const parsed = extractJSON<ScoringResult>(text) return ScoringResultSchema.parse(parsed) } function buildScoringPrompt(lead: Lead, icp: ICP): string { return `あなたはBtoB SaaS企業のGTMエンジニアです。以下のリード情報とICP(理想的顧客プロファイル)を照合し、リードスコアを算出してください。 ## ICP定義 - ターゲット業種: ${icp.targetIndustries.join(', ')} - 従業員規模: ${icp.employeeRange.min}${icp.employeeRange.max}- 技術シグナル(利用中なら加点): ${icp.techSignals.join(', ')} - 予算指標: ${icp.budgetIndicators.join(', ')} - 想定ペイン: ${icp.painPoints.join(', ')} - 除外条件: ${icp.disqualifiers.join(', ')} ## リード情報 - 企業名: ${lead.company.name} - 業種: ${lead.company.industry} - 従業員数: ${lead.company.employeeCount}- 年間売上: ${lead.company.annualRevenue ? `${lead.company.annualRevenue}万円` : '不明'} - 技術スタック: ${lead.company.techStack.join(', ') || '不明'} - 直近ニュース: ${lead.company.recentNews.join(' / ') || 'なし'} - 採用中の職種: ${lead.company.hiringRoles.join(', ') || '不明'} - 担当者: ${lead.contact.name}${lead.contact.title}- リードソース: ${lead.source} ## 採点基準(各25点満点、合計100点) 1. **industryFit**: 業種がICPターゲットに合致するか 2. **sizeFit**: 従業員規模がICPレンジ内か 3. **techFit**: 技術スタックにICPシグナルが含まれるか 4. **intentSignals**: ニュース・採用情報・リードソースから購買意欲が推定できるか ## 出力 以下のJSON形式で出力してください。JSONのみを出力し、他のテキストは含めないでください。 { "score": <0-100の整数>, "breakdown": { "industryFit": <0-25>, "sizeFit": <0-25>, "techFit": <0-25>, "intentSignals": <0-25> }, "reasoning": "<スコアの根拠を2-3文で>", "suggestedAction": "<immediate_outreach | nurture_sequence | disqualified | needs_enrichment>", "disqualifyReason": "<disqualifiedの場合のみ理由を記載>" }` } function extractJSON<T>(text: string): T { const jsonMatch = text.match(/\{[\s\S]*\}/) if (!jsonMatch) { throw new Error('Failed to extract JSON from response') } return JSON.parse(jsonMatch[0]) as T }

HubSpot CRM連携

スコアリング結果をHubSpotに書き戻す。

typescript
// src/crm/hubspot.ts import { z } from 'zod' import type { Lead } from '../types' import type { ScoringResult } from '../scoring/lead-scorer' const HubSpotConfig = z.object({ apiKey: z.string(), baseUrl: z.string().default('https://api.hubapi.com'), }) interface HubSpotContactProperties { email: string firstname: string lastname: string company: string jobtitle: string ai_lead_score: string ai_score_breakdown: string ai_suggested_action: string ai_reasoning: string } export async function upsertContactWithScore( lead: Lead, score: ScoringResult, config: z.infer<typeof HubSpotConfig> ): Promise<{ contactId: string }> { const [firstname, ...lastParts] = lead.contact.name.split(' ') const lastname = lastParts.join(' ') const properties: HubSpotContactProperties = { email: lead.contact.email, firstname, lastname, company: lead.company.name, jobtitle: lead.contact.title, ai_lead_score: String(score.score), ai_score_breakdown: JSON.stringify(score.breakdown), ai_suggested_action: score.suggestedAction, ai_reasoning: score.reasoning, } const response = await fetch( `${config.baseUrl}/crm/v3/objects/contacts`, { method: 'POST', headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ properties }), } ) if (!response.ok) { const error = await response.text() throw new Error(`HubSpot API error: ${response.status} ${error}`) } const data = await response.json() as { id: string } return { contactId: data.id } } export async function createDeal( contactId: string, lead: Lead, score: ScoringResult, config: z.infer<typeof HubSpotConfig> ): Promise<{ dealId: string }> { const dealProperties = { dealname: `${lead.company.name} - ${lead.contact.title}`, pipeline: 'default', dealstage: score.suggestedAction === 'immediate_outreach' ? 'qualifiedtobuy' : 'appointmentscheduled', amount: String(estimateDealValue(lead.company)), ai_hypothesis: score.reasoning, } const response = await fetch( `${config.baseUrl}/crm/v3/objects/deals`, { method: 'POST', headers: { 'Authorization': `Bearer ${config.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ properties: dealProperties, associations: [{ to: { id: contactId }, types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 3, }], }], }), } ) if (!response.ok) { const error = await response.text() throw new Error(`HubSpot Deal creation error: ${response.status} ${error}`) } const data = await response.json() as { id: string } return { dealId: data.id } } function estimateDealValue(company: { employeeCount: number }): number { // 従業員数ベースの概算(年間契約金額) if (company.employeeCount >= 500) return 5000000 if (company.employeeCount >= 100) return 2000000 if (company.employeeCount >= 50) return 1000000 return 500000 }

スコアリングAPIエンドポイント

typescript
// src/routes/scoring.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { LeadSchema } from '../types' import { scoreLead } from '../scoring/lead-scorer' import { defaultICP } from '../scoring/icp' import { upsertContactWithScore, createDeal } from '../crm/hubspot' const scoring = new Hono() scoring.post( '/score', zValidator('json', LeadSchema), async (c) => { const lead = c.req.valid('json') const score = await scoreLead(lead, defaultICP) const hubspotConfig = { apiKey: process.env.HUBSPOT_API_KEY ?? '', baseUrl: 'https://api.hubapi.com', } const { contactId } = await upsertContactWithScore( lead, score, hubspotConfig ) if (score.suggestedAction === 'immediate_outreach') { const { dealId } = await createDeal( contactId, lead, score, hubspotConfig ) return c.json({ score, contactId, dealId, action: 'deal_created', }) } return c.json({ score, contactId, action: score.suggestedAction, }) } ) export { scoring }

ユースケース2: 商談前リサーチ自動化

企業のIR資料、プレスリリース、採用ページを収集し、Claude APIで構造化データに変換する。営業が商談に入る前に「この企業は何に困っていそうか」を自動で仮説構築する。

リサーチ結果の型定義

typescript
// src/research/types.ts import { z } from 'zod' export const ResearchResultSchema = z.object({ company: z.string(), researchedAt: z.string().datetime(), summary: z.object({ overview: z.string(), recentStrategy: z.string(), financialHighlights: z.string(), }), painHypotheses: z.array(z.object({ hypothesis: z.string(), evidence: z.string(), confidence: z.enum(['high', 'medium', 'low']), relevantProduct: z.string(), })), talkingPoints: z.array(z.string()), risks: z.array(z.string()), competitorMentions: z.array(z.string()), }) export type ResearchResult = z.infer<typeof ResearchResultSchema>

ウェブスクレイピング + Claude API

typescript
// src/research/company-researcher.ts import { anthropic } from '../index' import type { Company } from '../types' import { ResearchResultSchema, type ResearchResult } from './types' interface PageContent { url: string title: string text: string fetchedAt: string } async function fetchPageContent(url: string): Promise<PageContent> { const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; GTMBot/1.0)', }, }) if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.status}`) } const html = await response.text() // HTMLからテキストを抽出(簡易版。本番ではReadabilityやcheerioを使う) const text = html .replace(/<script[\s\S]*?<\/script>/gi, '') .replace(/<style[\s\S]*?<\/style>/gi, '') .replace(/<[^>]+>/g, ' ') .replace(/\s+/g, ' ') .trim() .slice(0, 8000) // トークン制限を考慮して切り詰め return { url, title: html.match(/<title>(.*?)<\/title>/i)?.[1] ?? '', text, fetchedAt: new Date().toISOString(), } } async function collectCompanyPages( company: Company ): Promise<PageContent[]> { const urls = buildTargetUrls(company.domain) const results: PageContent[] = [] for (const url of urls) { try { const content = await fetchPageContent(url) results.push(content) } catch { // 取得失敗は無視して次へ continue } } return results } function buildTargetUrls(domain: string): string[] { const base = domain.replace(/\/$/, '') return [ base, `${base}/about`, `${base}/company`, `${base}/careers`, `${base}/recruit`, `${base}/news`, `${base}/press`, `${base}/ir`, `${base}/investor`, ] } export async function researchCompany( company: Company ): Promise<ResearchResult> { const pages = await collectCompanyPages(company) if (pages.length === 0) { throw new Error(`No pages could be fetched for ${company.name}`) } const pagesContext = pages .map((p) => `### ${p.title} (${p.url})\n${p.text}`) .join('\n\n---\n\n') const prompt = `あなたはBtoB SaaS企業のプリセールスリサーチャーです。以下の企業情報とWebページの内容を分析し、商談前リサーチレポートを作成してください。 ## 企業基本情報 - 企業名: ${company.name} - 業種: ${company.industry} - 従業員数: ${company.employeeCount}- 技術スタック: ${company.techStack.join(', ') || '不明'} ## 収集したWebページ ${pagesContext} ## 出力要件 以下のJSON形式で出力してください。JSONのみを出力し、マークダウンのコードブロックや他のテキストは含めないでください。 { "company": "${company.name}", "researchedAt": "${new Date().toISOString()}", "summary": { "overview": "<企業概要を3文以内で>", "recentStrategy": "<直近の経営戦略・注力領域を2文で>", "financialHighlights": "<業績・財務状況のポイント。不明なら「公開情報なし」>" }, "painHypotheses": [ { "hypothesis": "<この企業が抱えていそうな課題>", "evidence": "<その根拠(ページ内容からの引用・推定)>", "confidence": "<high|medium|low>", "relevantProduct": "<自社プロダクトのどの機能が刺さるか>" } ], "talkingPoints": ["<商談で話すべきポイント1>", "<ポイント2>", "<ポイント3>"], "risks": ["<商談リスク1>", "<リスク2>"], "competitorMentions": ["<ページ内で言及されていた競合サービス名>"] } ## 重要な注意 - painHypothesesは3つ生成してください - talkingPointsは3つ生成してください - 推測が入る場合はconfidenceをlowにしてください - Webページに情報がない場合は「公開情報なし」と明記し、推測で埋めないでください` const response = await anthropic.messages.create({ model: 'claude-sonnet-4-5-20250514', max_tokens: 2048, messages: [{ role: 'user', content: prompt }], }) const text = response.content[0].type === 'text' ? response.content[0].text : '' const jsonMatch = text.match(/\{[\s\S]*\}/) if (!jsonMatch) { throw new Error('Failed to extract JSON from research response') } return ResearchResultSchema.parse(JSON.parse(jsonMatch[0])) }

リサーチAPIエンドポイント

typescript
// src/routes/research.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { CompanySchema } from '../types' import { researchCompany } from '../research/company-researcher' const research = new Hono() research.post( '/research', zValidator('json', CompanySchema), async (c) => { const company = c.req.valid('json') const result = await researchCompany(company) return c.json({ result, tokenEstimate: estimateTokens(result), }) } ) function estimateTokens(result: unknown): number { const text = JSON.stringify(result) // 日本語は1文字約2-3トークン return Math.ceil(text.length * 2.5) } export { research }

ユースケース3: パーソナライズドメッセージ生成

リサーチ結果を元に、企業ごと・担当者ごとにカスタマイズしたアウトリーチメッセージを生成する。

メッセージ生成の型定義

typescript
// src/messaging/types.ts import { z } from 'zod' export const MessageVariantSchema = z.object({ variantId: z.string(), subject: z.string(), body: z.string(), cta: z.string(), tone: z.enum(['formal', 'casual', 'consultative']), focusAngle: z.string(), }) export const MessageSetSchema = z.object({ companyName: z.string(), contactName: z.string(), variants: z.array(MessageVariantSchema).min(2).max(4), qualityCheck: z.object({ hasPersonalization: z.boolean(), mentionsCompanyPain: z.boolean(), hasClearCTA: z.boolean(), wordCount: z.number(), spamRiskScore: z.number().min(0).max(100), }), }) export type MessageVariant = z.infer<typeof MessageVariantSchema> export type MessageSet = z.infer<typeof MessageSetSchema>

メッセージ生成 + AI品質チェック

typescript
// src/messaging/message-generator.ts import { anthropic } from '../index' import type { Lead } from '../types' import type { ResearchResult } from '../research/types' import { MessageSetSchema, type MessageSet } from './types' interface ProductInfo { name: string valueProposition: string features: string[] caseStudies: string[] } export async function generateMessages( lead: Lead, research: ResearchResult, product: ProductInfo ): Promise<MessageSet> { const prompt = `あなたはBtoB SaaS企業のセールスライターです。以下の情報を元に、アウトリーチメールのバリエーションを生成してください。 ## 自社プロダクト情報 - プロダクト名: ${product.name} - 価値提案: ${product.valueProposition} - 主要機能: ${product.features.join(', ')} - 導入事例: ${product.caseStudies.join(' / ')} ## ターゲット企業リサーチ - 企業名: ${research.company} - 概要: ${research.summary.overview} - 経営戦略: ${research.summary.recentStrategy} - 課題仮説: ${research.painHypotheses.map((h) => ` - [${h.confidence}] ${h.hypothesis}(根拠: ${h.evidence}`).join('\n')} ## 送信先担当者 - 氏名: ${lead.contact.name} - 役職: ${lead.contact.title} - 部署: ${lead.contact.department ?? '不明'} ## 生成ルール 1. 3つのバリエーションを生成する 2. 各バリエーションは異なるアングル(課題仮説)にフォーカスする 3. 件名は30文字以内。企業名か担当者名を含める 4. 本文は200文字以内。冗長な挨拶は省く 5. CTAは具体的な行動を1つだけ提示する(「15分のお打ち合わせ」等) 6. 絶対に嘘の実績・数字を入れない 7. 担当者の役職に合わせたトーンにする(CxOならconsultative、マネージャーならcasual) ## 出力 以下のJSON形式で出力してください。JSONのみを出力してください。 { "companyName": "${research.company}", "contactName": "${lead.contact.name}", "variants": [ { "variantId": "A", "subject": "<件名>", "body": "<本文>", "cta": "<CTA文>", "tone": "<formal|casual|consultative>", "focusAngle": "<どの課題仮説にフォーカスしたか>" } ], "qualityCheck": { "hasPersonalization": true, "mentionsCompanyPain": true, "hasClearCTA": true, "wordCount": <本文の文字数>, "spamRiskScore": <0-100。低いほど良い> } }` const response = await anthropic.messages.create({ model: 'claude-sonnet-4-5-20250514', max_tokens: 2048, messages: [{ role: 'user', content: prompt }], }) const text = response.content[0].type === 'text' ? response.content[0].text : '' const jsonMatch = text.match(/\{[\s\S]*\}/) if (!jsonMatch) { throw new Error('Failed to extract JSON from message response') } const messageSet = MessageSetSchema.parse(JSON.parse(jsonMatch[0])) // AI品質チェック const qualityResult = await checkMessageQuality(messageSet) return { ...messageSet, qualityCheck: qualityResult, } } async function checkMessageQuality( messageSet: MessageSet ): Promise<MessageSet['qualityCheck']> { const prompt = `以下の営業メールセットの品質をチェックしてください。 ${messageSet.variants.map((v) => `### バリエーション ${v.variantId} 件名: ${v.subject} 本文: ${v.body} CTA: ${v.cta}`).join('\n\n')} 以下のJSON形式で出力してください。JSONのみを出力してください。 { "hasPersonalization": <企業固有の情報が含まれているか: true/false>, "mentionsCompanyPain": <企業の課題に言及しているか: true/false>, "hasClearCTA": <明確な行動喚起があるか: true/false>, "wordCount": <全バリエーションの平均文字数>, "spamRiskScore": <0-100。スパム判定されやすい表現の多さ。0が最も安全> }` const response = await anthropic.messages.create({ model: 'claude-sonnet-4-5-20250514', max_tokens: 256, messages: [{ role: 'user', content: prompt }], }) const text = response.content[0].type === 'text' ? response.content[0].text : '' const jsonMatch = text.match(/\{[\s\S]*\}/) if (!jsonMatch) { return messageSet.qualityCheck } return JSON.parse(jsonMatch[0]) as MessageSet['qualityCheck'] }

メッセージ生成APIエンドポイント

typescript
// src/routes/messaging.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' import { LeadSchema } from '../types' import { ResearchResultSchema } from '../research/types' import { generateMessages } from '../messaging/message-generator' const ProductInfoSchema = z.object({ name: z.string(), valueProposition: z.string(), features: z.array(z.string()), caseStudies: z.array(z.string()), }) const MessageRequestSchema = z.object({ lead: LeadSchema, research: ResearchResultSchema, product: ProductInfoSchema, }) const messaging = new Hono() messaging.post( '/generate', zValidator('json', MessageRequestSchema), async (c) => { const { lead, research, product } = c.req.valid('json') const messages = await generateMessages(lead, research, product) return c.json({ messages, metadata: { variantCount: messages.variants.length, generatedAt: new Date().toISOString(), }, }) } ) export { messaging }

パイプライン全体のアーキテクチャ

3つのユースケースを1本のパイプラインとして統合する。

[Trigger: 新規リード登録]
  → [データ収集: 企業情報エンリッチメント]
  → [AI: リードスコアリング]
  → [分岐: スコア > 70]
    → High: [AI: 課題仮説生成] → [AI: メッセージ生成] → [CRM: Deal作成 + 営業通知]
    → Low:  [CRM: ナーチャリング登録] → [メールシーケンス開始]

パイプラインオーケストレーター

typescript
// src/pipeline/orchestrator.ts import type { Lead } from '../types' import { scoreLead, type ScoringResult } from '../scoring/lead-scorer' import { defaultICP } from '../scoring/icp' import { researchCompany } from '../research/company-researcher' import type { ResearchResult } from '../research/types' import { generateMessages } from '../messaging/message-generator' import type { MessageSet } from '../messaging/types' import { upsertContactWithScore, createDeal, } from '../crm/hubspot' const SCORE_THRESHOLD = 70 interface PipelineResult { lead: Lead score: ScoringResult research: ResearchResult | null messages: MessageSet | null action: 'deal_created' | 'nurture_registered' contactId: string dealId: string | null processingTimeMs: number } const defaultProduct = { name: 'SalesFlow', valueProposition: '営業チームのパイプライン管理を自動化し、商談速度を2倍にする', features: [ 'AIリードスコアリング', 'CRM自動連携', 'パーソナライズドメッセージ', 'パイプラインダッシュボード', ], caseStudies: [ 'A社: リード対応時間を80%短縮', 'B社: 商談化率が35%向上', 'C社: 営業1人あたりの商談数が2.5倍', ], } export async function processLead(lead: Lead): Promise<PipelineResult> { const startTime = Date.now() const hubspotConfig = { apiKey: process.env.HUBSPOT_API_KEY ?? '', baseUrl: 'https://api.hubapi.com', } // Step 1: スコアリング const score = await scoreLead(lead, defaultICP) // Step 2: CRMにコンタクト登録 const { contactId } = await upsertContactWithScore( lead, score, hubspotConfig ) // Step 3: スコアに応じて分岐 if (score.score >= SCORE_THRESHOLD) { // High Score: リサーチ → メッセージ生成 → Deal作成 const research = await researchCompany(lead.company) const messages = await generateMessages(lead, research, defaultProduct) const { dealId } = await createDeal( contactId, lead, score, hubspotConfig ) await notifySalesTeam(lead, score, research, messages, dealId) return { lead, score, research, messages, action: 'deal_created', contactId, dealId, processingTimeMs: Date.now() - startTime, } } // Low Score: ナーチャリング登録 return { lead, score, research: null, messages: null, action: 'nurture_registered', contactId, dealId: null, processingTimeMs: Date.now() - startTime, } } async function notifySalesTeam( lead: Lead, score: ScoringResult, research: ResearchResult, messages: MessageSet, dealId: string ): Promise<void> { const slackWebhookUrl = process.env.SLACK_SALES_WEBHOOK_URL if (!slackWebhookUrl) return const topHypothesis = research.painHypotheses[0] await fetch(slackWebhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ blocks: [ { type: 'header', text: { type: 'plain_text', text: `New High-Score Lead: ${lead.company.name}`, }, }, { type: 'section', fields: [ { type: 'mrkdwn', text: `*Score:* ${score.score}/100` }, { type: 'mrkdwn', text: `*Contact:* ${lead.contact.name} (${lead.contact.title})` }, { type: 'mrkdwn', text: `*Top Pain:* ${topHypothesis?.hypothesis ?? 'N/A'}` }, { type: 'mrkdwn', text: `*Message Variants:* ${messages.variants.length}` }, ], }, { type: 'actions', elements: [ { type: 'button', text: { type: 'plain_text', text: 'View Deal' }, url: `https://app.hubspot.com/contacts/deals/${dealId}`, }, ], }, ], }), }) }

Webhookエンドポイント(パイプラインの入口)

typescript
// src/routes/pipeline.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { LeadSchema } from '../types' import { processLead } from '../pipeline/orchestrator' const pipeline = new Hono() pipeline.post( '/webhook/new-lead', zValidator('json', LeadSchema), async (c) => { const lead = c.req.valid('json') try { const result = await processLead(lead) return c.json({ status: 'processed', action: result.action, score: result.score.score, processingTimeMs: result.processingTimeMs, }) } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' return c.json( { status: 'error', message }, 500 ) } } ) export { pipeline }

アプリケーションのエントリポイント

typescript
// src/app.ts import { Hono } from 'hono' import { logger } from 'hono/logger' import { scoring } from './routes/scoring' import { research } from './routes/research' import { messaging } from './routes/messaging' import { pipeline } from './routes/pipeline' const app = new Hono() app.use('*', logger()) app.route('/api/scoring', scoring) app.route('/api/research', research) app.route('/api/messaging', messaging) app.route('/api/pipeline', pipeline) app.get('/health', (c) => c.json({ status: 'ok' })) export default { port: Number(process.env.PORT ?? 3000), fetch: app.fetch, }

コスト試算

1リードあたりの処理コスト

Claude Sonnet 4.5の料金(2026年4月時点)で計算する。

処理ステップ入力トークン出力トークンコスト(USD)
リードスコアリング~800~300$0.0039
企業リサーチ(3ページ分析)~6,000~800$0.0204
メッセージ生成(3バリエーション)~2,000~600$0.0078
品質チェック~500~200$0.0021
合計(High Scoreリード)~9,300~1,900$0.0342
合計(Low Scoreリード)~800~300$0.0039

※ Claude Sonnet 4.5: 入力 $3/1M tokens、出力 $15/1M tokens で計算

月間コスト試算

月間1,000リード処理を想定。High Score率を30%とする。

項目数量単価月額
High Scoreリード処理300件$0.0342$10.26
Low Scoreリード処理700件$0.0039$2.73
Claude API合計$12.99
HubSpot CRM--$0(無料プラン)
Hono サーバー(Cloudflare Workers)--$5
月額合計約$18

月額約$18で1,000リードの自動スコアリング・リサーチ・メッセージ生成が回る。

ROI計算

SDR 1人が手動で同等の作業をした場合との比較。

項目SDR手動AI自動化
1リードの処理時間30-60分10-30秒
月間処理可能数100-150件/人1,000件+(上限なし)
月間人件費60万円(1人分)約2,700円(API費用)
品質のばらつき担当者によって差がある一定
稼働時間平日9-18時24/7

1,000リード/月を処理する場合:

  • SDR: 7-10人必要 = 月420-600万円
  • AI自動化: 約2,700円 + QA担当1人(60万円)= 約60万円

年間削減額: 4,300-6,500万円

ただし注意点がある。AIが生成したメッセージをそのまま送信するのではなく、QA担当がサンプリングチェックする運用は必須だ。Vercelの事例でも「AIエージェント + QA1人」という構成だった。完全無人化ではなく、品質保証の人間を1人置く設計が現実的だと思う。

プロンプトエンジニアリングのTips

営業パイプラインでClaude APIを使う際のプロンプト設計のコツを4つ挙げる。

Tip 1: 出力フォーマットをzodスキーマで強制する

プロンプトにJSONスキーマを埋め込み、レスポンスのパース失敗を減らす。

typescript
// プロンプトの末尾に毎回入れるパターン const formatInstruction = ` 以下のJSON形式で出力してください。JSONのみを出力し、マークダウンのコードブロックや説明テキストは含めないでください。 ` // パース側も防御的に書く function safeParseJSON<T>(text: string, schema: z.ZodSchema<T>): T { // コードブロックの除去 const cleaned = text .replace(/```json\n?/g, '') .replace(/```\n?/g, '') .trim() // JSON部分の抽出 const jsonMatch = cleaned.match(/\{[\s\S]*\}/) if (!jsonMatch) { throw new Error(`No JSON found in response: ${text.slice(0, 200)}`) } const parsed = JSON.parse(jsonMatch[0]) return schema.parse(parsed) }

Tip 2: ハルシネーション対策(ファクトチェック付きプロンプト)

営業メッセージでの最大のリスクは、AIが存在しない実績や数字を生成すること。

typescript
const antiHallucinationInstruction = ` ## 重要な制約 - 提供された情報に含まれていない事実・数字・実績は絶対に記載しないでください - 推測が必要な場合は「〜と推察します」「〜の可能性があります」と明示してください - 企業のWebページに記載がない情報は「公開情報なし」としてください - 自社プロダクトの導入事例は、上記で提供したものだけを使ってください `

これをプロンプトの冒頭に入れる。末尾ではなく冒頭に置くことで、生成全体にこの制約が効く。

Tip 3: 営業トーンの制御

CxO向けとマネージャー向けで文体を変える。

typescript
function getToneInstruction(title: string): string { const isCxO = /CEO|CTO|COO|CFO|VP|Director|部長|本部長|取締役|執行役員/i.test(title) if (isCxO) { return ` トーン: consultative(コンサルティング型) - 経営課題に対する洞察を示す - 数字・インパクトで語る - 具体的な15分の打ち合わせを提案する - 「ご検討いただけないでしょうか」ではなく「ご意見を伺えればと思います」 ` } return ` トーン: casual(カジュアル型) - 実務上の具体的な課題に寄り添う - 機能・操作性にフォーカスする - デモやトライアルを提案する - 「お気軽にお試しください」のような平易な表現 ` }

Tip 4: A/Bテストのフレームワーク

メッセージの3バリエーションにIDを振り、効果測定できるようにする。

typescript
interface ABTestConfig { experimentId: string variants: { id: string weight: number // 配信比率(合計1.0) focusAngle: string }[] } function selectVariant(config: ABTestConfig): string { const random = Math.random() let cumulative = 0 for (const variant of config.variants) { cumulative += variant.weight if (random <= cumulative) { return variant.id } } return config.variants[0].id } // 効果計測用のトラッキングパラメータ function buildTrackingUrl( baseUrl: string, experimentId: string, variantId: string ): string { const url = new URL(baseUrl) url.searchParams.set('utm_source', 'outbound') url.searchParams.set('utm_medium', 'email') url.searchParams.set('utm_campaign', experimentId) url.searchParams.set('utm_content', variantId) return url.toString() }

これでどのバリエーションの開封率・返信率が高いかを追跡できる。データが溜まったら、高パフォーマンスのバリエーションの特徴をプロンプトにフィードバックする。プロンプト自体のPDCAを回す。

次にやること

GTMエンジニアリングの4層モデルにおいて、AI処理層は判断と生成を担う。データ収集層から受け取った生データを、CRM出力層が扱える構造化データに変換する。この「変換」のロジックをTypeScriptで書くのがGTMエンジニアの仕事の核だ。

Claude APIへのリクエストを「投げて終わり」にしないこと。zodスキーマによる型検証、ハルシネーション対策のプロンプト制約、品質チェックの二段構え——プロダクト開発でバリデーションやテストを書くのと同じ感覚で、AI処理にもガードレールを設ける。

次の記事では、GTMエンジニアリングのインフラ比較として、n8n / Make / カスタムコード(Hono + Cloudflare Workers)のどれを選ぶべきかを、コスト・拡張性・運用負荷の観点から比較する。


参考資料

$ echo $TAGS
#Claude API#営業自動化#パイプライン#TypeScript#Hono#SLG