apify-buying-signal-detection について
求人、資金調達、LinkedIn の投稿から購買意向を示す企業を見つける定期パイプラインを設定するスキルです。Apify の収集と集約を別々にスケジュールし、重複を除いた根拠リンク付きリードを CSV に追加します。
主な機能
- 01ICP とシグナル種別の設定
- 02Apify Actor Tasks の作成とスケジュール
- 03求人・資金調達・LinkedIn シグナルの集約
- 04ブラックリスト適用と企業の重複排除
- 05週次の冪等性ガード
- 06根拠リンク付きリードの CSV 追加
活用シーン
- 01対象企業の採用や資金調達を監視
- 02関連する LinkedIn 投稿を行う企業を発見
- 03重複を除いたリード情報を定期的に維持
MCP 対応
Claude Code の scheduled-tasks MCP を使って集約スケジュールを登録・確認できます。cron、Task Scheduler、GitHub Actions で集約を実行する方法もあります。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ apify/ awesome-skills --skill apify-buying-signal-detectionインストール後、Claude Code で/apify-buying-signal-detectionを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.gitignoreexamples/blacklist.example.csvicp.example.jsonleads.example.csvreferences/actors.mdcsv-schema.mdgotchas.mdicp-config-schema.mdscripts/aggregate.pysetup_apify_tasks.py詳細
購買意向の検出
ICP の説明から、求人情報、資金調達イベント、LinkedIn の投稿という 3 種類のシグナルをもとに購買意向を示す企業を見つけ、重複を除いた単一の leads.csv に追加する定期的なパイプラインを構築します。このファイルはそのまま CRM に渡せます。
このスキルでできること(意図的に対象外としていること)
このスキルは、スケジュールされたワークフローを設定して実行します。コールドメールの作成、適合度に基づくリードのスコアリング、CRM への行の追加は行いません。出力は、証拠へのリンクが付いたクリーンな leads.csv です。これは、すでにお使いのアウトリーチプロセスに渡すための入力です。これらのリードへのコールドアウトリーチ文面の作成は、意図的に別のタスクとしています(そのために apify-link-prospecting-outreach などのスキルがあります)。
始める前に知っておくべき設計上の重要な方針は、次の 3 つです。
- Apify 側と Claude 側は、別々のスケジュールで実行されます。 Apify は独自の cron で Actor を実行し、Claude は独自の cron で集約処理を実行します。Actor の実行時に Claude Code を起動しておく必要はありません。この分離によって、「トリガーするのを忘れないようにする」だけではない、実際に繰り返し実行されるパイプラインになります。
- 週次の冪等性は、集約時に確保されます。 リード CSV に現在の ISO 週のエントリがすでにある場合、集約スクリプトは HTTP 呼び出しを行わずに早期終了します。Claude 側のスケジュールは、コストに影響を与えることなく、安全策として週 1 回より頻繁に実行できます。
- 重複排除では、最初に検出されたシグナルを優先します。 月曜日に求人シグナルで検出されたリードは、同じ企業が水曜日に資金調達フィードに表示されても、求人シグナルのリードのままです。企業を最初に検出したシグナルのほうが、より有用です。
前提条件
- Apify アカウント(登録)
- 次のいずれかによる認証:
apify login(Apify CLI を使用する場合の OAuth)APIFY_TOKEN環境変数- Apify Console → Settings → Integrations のトークン
- Python 3.10 以降(
scripts/aggregate.pyとscripts/setup_apify_tasks.py用。標準ライブラリのみが必要です。requestsが利用可能な場合は使用しますが、利用できない場合はurllibにフォールバックします) - 任意:Claude 側の集約処理をスケジュールに従って起動する方法。Claude Code を使用している場合は、ローカルの
scheduled-tasksMCP を推奨します。ヘッドレスで実行する場合は、cron、Task Scheduler、GitHub Actions も使用できます
ワークフロー
このチェックリストをコピーして、完了した項目にチェックを付けてください。
Task Progress:
- [ ] Step 1: Collect ICP inputs (block on these)
- [ ] Step 2: Write icp.json + blacklist.csv (if any)
- [ ] Step 3: Provision Apify Actor Tasks (setup_apify_tasks.py)
- [ ] Step 4: Verify Actor picks in the Apify Console
- [ ] Step 5: Register the Claude-side aggregation schedule
- [ ] Step 6: First manual run of aggregate.py — sanity check the output
ステップ 1:ICP の入力情報を収集する(これらがそろうまで先に進まない)
ファイルを書き込む前に、以下のすべてをユーザーに確認します。セットアップスクリプトで適切に振り分けるには各フィールドが必要です。また、プロビジョニング後にスケジュール済みタスクを修正するには、Apify Console で編集するか、セットアップを再実行する必要があります。どちらも一度確認するより手間がかかります。
- キャンペーン名 — 短いスラッグ(小文字、ハイフン区切り)。すべての Apify Task 名の接頭辞として使用します(例:
emea-saas-hiring-aes-bebity-linkedin-jobs-scraper)。ユーザーがすでに複数のキャンペーンを実施している場合は、事前に名前が重複しないようにします。 - 追跡するシグナル —
["jobs", "funding", "linkedin_content"]のサブセットです。キャンペーンでシグナルを 1 つだけ使うことはまれです。ワークフローの強みは、企業ごとのシグナルの組み合わせにあります。特別なコスト上の懸念がなければ、3 つすべてを推奨します。 - 地域(ISO 国コード) — 大文字の 2 文字コードです。地域ごとの Actor の振り分けに使われます(DE/AT/BE は Stepstone、AU/NZ は Seek、FR は France Travail、FR の資金調達情報は Maddyness)。グローバルキャンペーンでは、ユーザーが実際に販売しているすべての国を指定します。
["US", "GB", "DE", "FR", "AU"]を渡すと、地域別の求人 Actor が 5 つ実行され、週あたりのコストは 5 倍になります。references/gotchas.mdを参照してください。 - 業界キーワード — カテゴリーを表す語句です。資金調達トラッカーには
industryとして渡され、明示的なコンテンツ検索語句が指定されていない場合は LinkedIn にkeywordsとして渡されます。また、ペルソナの職種が指定されていない場合は、求人スクレーパーのフォールバックとして使われます。 - ペルソナ(求人シグナルが有効な場合) — ICP が採用する職種です。具体的な職種名はカテゴリーより有効です。
"Account Executive"、"SDR"、"BDR"は適切なヒットになりますが、"sales"はノイズになります。任意の役職レベル(entry、mid、senior、manager、director、vp、cxo)と企業規模の区分("11-50"など)は、集約時に後処理として適用されます。 - 資金調達の設定(資金調達シグナルが有効な場合) — ステージ(
seed、series_a、series_bなど)とmax_days_since_announcement(デフォルトは 90)です。資金調達が新しいほど、予算が確保され、期間を絞り込みやすくなります。 - LinkedIn コンテンツの設定(linkedin_content シグナルが有効な場合) — 検索フレーズです。品質を左右する最大の要因です。広すぎる語句(
"sales")は予算の無駄になります。カテゴリー名より、具体的な課題に関するフレーズが効果的です。references/actors.mdを参照してください。加えて、投稿のフィルタリング用にmin_reactions(デフォルトは 5)とposted_within_days(デフォルトは 14)を指定します。 - リードの保存先 — CSV ファイルのパスです。デフォルトはキャンペーンディレクトリ内の
./leads.csvです。このファイルは実行間でパイプラインの状態を記録します。重複排除のガードをディスクのリセット後も維持できるよう、バージョン管理下に置くか、少なくともバックアップしてください。 - ブラックリスト CSV のパス — 任意です。
domain,company,reason列を持つ CSV です。ドメインが完全一致する行、または正規化した企業名が一致する行は、追記前に除外されます。ブラックリストがない場合は、除外対象(既存顧客、自社ドメイン、主要な競合他社など)から始めるか尋ねます。 - スケジュール —
apify_side_cron(Apify が Actor を実行するタイミング)とclaude_side_cron(Claude が集約するタイミング)です。デフォルトは0 6 * * 1(Apify が月曜日の 06:00 UTC に実行)と0 8 * * 1(Claude が月曜日の 08:00 UTC に実行)です。間に 2 時間の余裕を設けることで、Actor の実行が遅れた場合にも対応できます。
スキーマ全体は references/icp-config-schema.md に記載されています。記入例は examples/icp.example.json にあります。
ステップ 2:icp.json と blacklist.csv を作成する
キャンペーンディレクトリの内容を書き込みます:
<campaign-dir>/
icp.json ← the config from Step 1
blacklist.csv ← optional; columns: domain,company,reason
leads.csv ← created empty; the aggregator will populate it
初回実行時に集約処理がファイルの欠落に対処しなくて済むよう、leads.csv はヘッダー行だけで開始します(スキーマは references/csv-schema.md を参照してください)。
detected_at,company,domain,signal_type,signal_detail,signal_source_actor,signal_date,evidence_url,geo,notes
ステップ 3:Apify Actor タスクをプロビジョニングする
セットアップスクリプトを実行します。
APIFY_TOKEN=$APIFY_TOKEN \
python ${CLAUDE_PLUGIN_ROOT}/scripts/setup_apify_tasks.py \
--config ./icp.json
このスクリプトの動作:
icp.jsonを読み込み、references/actors.mdのルーティングテーブルに従って Actor を選択します。グローバル Actor は常に選択され、地域別 Actor は地域リストに一致する場合に選択されます。- 選択された各 Actor について、ICP から導出した入力ペイロードを使い、
<campaign>-<actor-slug>という名前の Apify Actor タスクを作成または更新します。スクリプトを再実行すると既存のタスクがその場で更新され、重複は作成されません。 - タスク ID を記録したサイドカー
<campaign-dir>/.<campaign-name>.tasks.jsonを書き込みます。aggregate.pyはこれを読み込み、どのタスクからデータセット項目を取得するかを把握します。 - ICP で
schedule.apify_side_cronが設定されている場合(デフォルトで設定されています)、その cron ですべてのタスクを実行する Apify Schedule を 1 つ作成または更新します。
便利なフラグ:
--dry-run— API 呼び出しを行わずに、選択リストとペイロードを表示します。新しいキャンペーンを作成する際は、必ず一度実行してください。--no-schedule— タスクをプロビジョニングしますが、Schedule の作成はスキップします(調整中に手動で実行したい場合に便利です)。
ステップ 4:Apify Console で Actor の選択を確認する
console.apify.com/actors/tasks を開きます。キャンペーンのプレフィックスで絞り込み、次の 3 点を確認します。
- 適切な Actor が選択されていること — 地域別の Actor(Stepstone/Seek/France Travail/Maddyness)は、対象の地域に対してのみ実行されます。ICP に ANZ の国が含まれていないのに Seek が表示される場合は、何か問題があります。
- 入力ペイロードが正しいこと — 各タスクをクリックし、入力 JSON を表示します。キーワード、タイトル、ステージが ICP から入力されていて、
nullがないことを確認します。 - Apify Schedule が有効であること — Schedules で
<campaign>-scheduleを探し、有効になっていてすべてのタスクが一覧に含まれていることを確認します。
問題がある場合は、icp.json を編集して setup_apify_tasks.py を再実行します。冪等に実行できます。
ステップ 5:Claude 側の集約スケジュールを登録する
キャンペーンの claude_side_cron でアグリゲーターを呼び出すスケジュール済みタスクを登録します。環境に合った実行方法を選んでください。
オプション A — Claude Code 内の scheduled-tasks MCP。 MCP は mcp__scheduled-tasks__create_scheduled_task を公開しています。実行する具体的な呼び出しは次のとおりです。
{
"name": "<campaign-name>-aggregate",
"cron_expression": "<value of schedule.claude_side_cron from icp.json>",
"timezone": "UTC",
"prompt": "Run the buying-signal aggregator. Execute exactly: python ${CLAUDE_PLUGIN_ROOT}/scripts/aggregate.py --config /abs/path/to/icp.json. Requires APIFY_TOKEN env var. On non-zero exit, surface the stderr in the notification body — do not attempt to reinterpret the error."
}
呼び出しを行う前に、<campaign-name> と /abs/path/to/icp.json を実際の値に置き換えてください。mcp__scheduled-tasks__list_scheduled_tasks でタスクが登録されたことを確認し、cron が icp.json と一致していることを確認します。
オプション B — ヘッドレス cron/タスク スケジューラ/CI。 OS レベルのスケジューラに通常のエントリーを追加します。
# Linux crontab entry
0 8 * * 1 APIFY_TOKEN=$APIFY_TOKEN /path/to/python /path/to/aggregate.py --config /path/to/icp.json >> /path/to/aggregate.log 2>&1
または、GitHub Actions のワークフロー(.github/workflows/aggregate.yml)を使用します。
on:
schedule:
- cron: '0 8 * * 1'
jobs:
aggregate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11' }
- env: { APIFY_TOKEN: '${{ secrets.APIFY_TOKEN }}' }
run: python scripts/aggregate.py --config icp.json
どちらの実行方法も、週 1 回より頻繁に実行しても安全です。週次の冪等性ガードが、今週のエントリーがすでにある実行をスキップします。調整中のみ --force を使用してください。
ステップ 6:aggregate.py を初めて手動実行する
スケジュールが実行されるのを待つ前に、一度手動で実行して配線を確認します。これにより leads.csv に初期データが作成され、翌週に週次ガードが比較できるようになります。
APIFY_TOKEN=$APIFY_TOKEN \
python ${CLAUDE_PLUGIN_ROOT}/scripts/aggregate.py --config ./icp.json
想定される出力形式:
{
"summary": {
"appended": 47,
"fetched_by_signal": {"jobs": 320, "funding": 88, "linkedin_content": 210},
"dropped": {"blacklist": 2, "dup_domain": 156, "dup_url": 401, "post_filter": 12, "unmappable": 0, "linkedin_no_domain": 8},
"linkedin_profile_lookups": 142,
"linkedin_profile_resolved": 134,
"linkedin_company_lookups": 118,
"linkedin_company_resolved": 112,
"linkedin_domain_resolved": 128
}
}
wrote 47 new rows to /abs/path/to/leads.csv
linkedin_*_lookups/linkedin_*_resolved カウンターは、LinkedIn の作成者ドメインを補完するチェーンの各段階を報告します(詳細は以下を参照)。linkedin_domain_resolved は、domain 列への入力に成功した LinkedIn 行の数です。linkedin_no_domain は、チェーンでドメインを解決できなかったため除外された LinkedIn 行の数です。これらの行は安全にブラックリスト登録したり、重複排除したりできません。
fetched_by_signal がすべてゼロの場合、Apify タスクがまだ実行されていない(Console を確認してください)か、サイドカーのタスクレジストリが不足しています。最初の Apify 実行が完了するまで待ってから、再実行してください。
--dry-run を使用すると CSV に変更を加えずに追記される内容を確認でき、--force を使用すると調整中に週次ガードを回避できます。
Actor のルーティング
シグナルごとの PICK ルールを含む完全なカタログは、references/actors.md にあります。要約は以下のとおりです。
| シグナル | 全体のデフォルト | 地域別の追加 |
|---|---|---|
| 求人 | bebity/linkedin-jobs-scraper、johnvc/google-jobs-scraper | Indeed(US/GB/IN/CA)、Stepstone(DE/AT/BE)、Seek(AU/NZ)、France Travail(FR) |
| 資金調達 | nexgendata/startup-funding-tracker、memo23/crunchbase-scraper、complex_intricate_networks/fundraising-and-startup-funding-scraper、signalbase/signalbase-api | Maddyness(FR) |
| LinkedIn コンテンツ | harvestapi/linkedin-post-search(Cookie 不要、投稿 1,000 件あたり $2) | ディープスクレイプのフォールバック:curious_coder/linkedin-post-search-scraper(Cookie 必須) |
LinkedIn の著者 → 企業ドメイン(エンリッチメント。aggregate.py からオンデマンドで呼び出し) | harvestapi/linkedin-profile-scraper(プロフィール 1,000 件あたり $4)および harvestapi/linkedin-company(ルックアップごと)— 2 段階 | なし — プロフィール URL と企業の LinkedIn URL が主キーです |
setup_apify_tasks.py::pick_actors のルーティングロジックはこの表を反映しています。一方を編集した場合は、もう一方も編集してください。
LinkedIn のエンリッチメントを定期実行ではなくオンデマンドで行う理由と、2 段階の処理にする理由
harvestapi/linkedin-post-search は著者の名前と見出しを返しますが、勤務先のウェブサイトは返しません。ドメインがないと、アグリゲーターはこのシグナルについて、ブラックリストを確認したり、以前に確認した企業と重複していないか判定したりできません。そのため、ブラックリストに登録された競合他社が LinkedIn の投稿経由で紛れ込む可能性があります。
ドメインの特定には、aggregate.py::enrich_linkedin_domains 内で連鎖して実行される追加の Actor 呼び出しが 2 回必要です。
harvestapi/linkedin-profile-scraperを、投稿データにドメインが含まれずに返された著者プロフィール URL の重複排除済みセットに対して実行します。currentPosition[0].companyLinkedinUrlとcompanyNameを返しますが、企業のウェブサイトは返しません。入力:{profileScraperMode: "Profile details no email ($4 per 1k)", urls: [...]}。harvestapi/linkedin-companyを、手順 1 で返された企業 LinkedIn URL の重複排除済みセットに対して実行します。websiteを返します。入力:{companies: [...]}。
この処理は、アグリゲーターにおける唯一の同期 Actor 呼び出し経路です。それ以外のすべてのデータは、事前にスケジュールされたタスク実行から取得されます。エンリッチメントへの両方の入力(著者プロフィール URL、その後の企業 URL)は、前の段階のデータセットを読み込んだ後でしか把握できないため、これは意図的な例外です。
コスト。
- プロフィールのスクレイピング:プロフィール 1,000 件あたり $4(ドメインの特定にメールアドレスの照会は不要なため、「no email」の料金プランを選択)
- 企業のスクレイピング:
harvestapi/linkedin-companyのイベントごとの従量課金 - 合計の目安:LinkedIn の投稿 500 件で著者 1 人あたり平均 3 件の投稿があるキャンペーンでは、プロフィールのルックアップが約 170 回、企業のルックアップが約 150 回になります(多くの著者が同じ企業に勤務しています)。
コストを抑える仕組みは 3 つあります。
- 正規化されたプロフィール URL の重複排除 — 同じ著者による投稿が N 件あっても、プロフィールのルックアップは 1 回です(
canonical_linkedin_profile_urlは?miniProfileUrn=…を取り除くため、サンプル投稿間で同じ著者は 1 つのキーにまとめられます)。 - 企業スクレイパーの段階で企業 URL を重複排除 — 同じ企業に勤務する著者が N 人いても、企業のルックアップは 1 回です。
- すべての LinkedIn 行にすでにドメインが含まれている場合、処理全体をスキップします。
Actor の呼び出し — インターフェースの選択
setup_apify_tasks.py は Apify REST API を直接使用します(Actor の呼び出しは行わず、タスクとスケジュールを作成します)。aggregate.py は REST API を使用して、各タスクの最後に成功した実行からデータセット項目を取得します。トラブルシューティング中に Actor を手動で実行する場合(例:ステップ 4 の検証)は、以下のいずれかを使用してください。
オプション A:Apify CLI(移植性を重視する場合に推奨)
各呼び出しで3つのフラグを指定します(--json、--user-agent、2>/dev/null)。
# キャンペーンのタスクを手動で1つ実行する
apify tasks run <task-id> --wait 300 \
--json \
--user-agent apify-awesome-skills/apify-buying-signal-detection \
2>/dev/null
# このキャンペーン用にプロビジョニングされたタスクを一覧表示する
apify tasks list --json 2>/dev/null | \
jq '.[] | select(.name | startswith("<campaign-name>-"))'
# タスクの最新データセットを確認する
apify tasks last-run <task-id> --dataset --format json \
--user-agent apify-awesome-skills/apify-buying-signal-detection 2>/dev/null
# Actor の入力スキーマを取得する(ルーティングテーブルに追加するか判断する場合)
apify actors info "<actor-id>" --input --json \
--user-agent apify-awesome-skills/apify-buying-signal-detection 2>/dev/null
オプション B:Apify MCP コネクター
mcp.apify.com でホストされている MCP サーバーです。詳細なドキュメントは docs.apify.com/platform/integrations/mcp を参照してください。
オプション C:任意の MCP クライアント(例:mcpc)
スタンドアロンの CLI クライアントです。github.com/apify/mcpc を参照してください。
トラブルシューティング
| エラー/症状 | 対処方法 |
|---|---|
APIFY_TOKEN not found | export APIFY_TOKEN=$(cat ~/.apify_token) を実行するか、.env に追加してください。console.apify.com/account/integrations で取得できます。 |
no task registry for campaign '<name>' | setup_apify_tasks.py を実行する前に aggregate.py を実行しました。先にセットアップを実行してください。アグリゲーターが必要とするサイドカーが書き込まれます。 |
正当な再実行で skipped: already run this week と表示される | --force を渡してください。ガード機能はその週のエントリーを保持して重複を除去します。上書きはしません。 |
Apify でタスクが実行されるが、aggregate.py が "fetched_by_signal": {"jobs": 0} と報告する | Actor は実行されましたが、アイテムを0件返しました。Actor の実行ログでスキーマエラー(キーワード形式の誤り、未対応の国コードなど)を確認してください。修正後、apify tasks run でタスクを手動実行し、再集約してください。 |
| タスクはプロビジョニングされているが、データが一度も届かない | Apify スケジュールが無効になっている可能性があります。Console で Schedules → <campaign>-schedule を開き、有効になっていることを確認してください。また、スケジュールの cron が想定するタイムゾーンと一致しているか確認してください。timezone を設定しない限り、スケジュールは UTC で実行されます。 |
| コストが予想より高い | references/gotchas.md#cost-guardrails を参照してください。最もよくある原因は、LinkedIn の検索語句が広すぎて harvestapi/linkedin-post-search のコストが増大することです。次によくある原因は、ICP が実際には2か国にしか販売していないのに、地域別の求人 Actor をすべて追加することです。 |
| 再投稿によって LinkedIn のシグナル数が膨らむ | アグリゲーターは、重複除去の前に URL を正規化するため、trackingId と utm_* クエリパラメーターを除去します。ただし、LinkedIn の URL 形式は定期的に変わります。同じ投稿が2回表示される場合は、除去リストにないパラメーターだけが URL 間で異なっていないか確認し、該当するパラメーターを aggregate.py の strip_tracking() に追加してください。 |
| 企業のリブランド後にリードが重複する | 重複除去は domain を優先します。企業がドメインを変更すると、アグリゲーターは新しいリードとして扱います。手動での照合のみ対応しており、自動で解決する方法はありません。 |
複数のマシンが同じ leads.csv に書き込む | サポートされていません。単一の書き込み元を前提としています。CSV をロック機構(Google Sheets のエクスポート、flock など)の背後に置くか、マシンごとに分割してください。 |