OpenOutreachは、製品を1文で説明すると条件に合う人物を見つけ、なぜその人を選んだのかを文章で書き出すオープンソースのAIリード発掘ツールだ。ただしこの説明が当てはまるようになったのは2026年8月19日以降で、それより前は「見つけて、あなたのメールボックスから送る」ツールだった。リポジトリの説明文は新しい姿に更新済みだが、検索エンジンが返す要約、リポジトリに残るトピックタグ、そして本記事の旧版は、いまも消えた側の機能を説明している。この記事では、リポジトリの差分とCLIの実測で現在の姿を確定させる。
- 入力はリストではなく1文:「DevOpsチーム向けのクラウドコスト最適化SaaS」のような製品説明と対象市場を書くと、LLMが検索語に変換して候補を集める
- 出力は行ではなく判定:1人ごとにLLMが適合を判定し、選んだ理由を
reason列に文章で残す。スコア列は意図的に持たない - 2026年8月19〜20日に送信機能を全削除:タグ
pre-finder-cutとmainの差分で129ファイル・11,705行が消えた。送信は別リポジトリへ分離 - スクレイピングしない:ブラウザもソーシャルアカウントも使わず、候補はライセンス契約したデータ提供元から取得する
- READMEの推奨インストールは現時点で通らない:
uvx openoutreachはPyPI未公開のため失敗する(2026-08-21 JST実測)
① 2026年8月19〜20日に送信機能が11,705行分削除された。リポジトリの説明文は更新済みだが、検索結果の要約とトピックタグ
email-automation は古い姿のまま② スクレイピングは行わないが
linkedin_url 列は残る——収集した値ではなく、ライセンス提供元のペイロードに含まれるURLだから③ READMEが推奨する
uvx openoutreach は2026-08-21 JST時点で通らない。動くのはソース導入とコンテナイメージ
自動化ツール全体の地図から先に把握したい場合は AI自動化ツール|ノーコードからコードまで2026年版の比較と選び方 を参照してほしい。
OpenOutreachとは——リストを持ち込まないAIリード発掘ツール
OpenOutreachはPythonで書かれたセルフホスト型のリード発掘ツールで、ライセンスはGPLv3だ。GitHubのAPIがライセンスを NOASSERTION と返すのは、ライセンスファイル名が英式綴りの LICENCE.md になっていて自動判定にかからないためで、pyproject.toml には license = "GPL-3.0-or-later" と license-files = ["LICENCE.md"] が明記されている。
このツールの設計上の主張は2点に集約される。
・リストを持ち込まない:一般的なコールドメール配信ツールは、送る相手のリストを利用者が用意して取り込む。OpenOutreachは取り込むものが無く、入力は製品についての文章そのものである
・出力が行ではなく判定である:リードデータベースが返すのは条件に一致した行の集合だが、OpenOutreachが返すのは1人ごとの適合判定と、その根拠を書いた文章である。判定がずれていたら製品説明の文章を直す、という修正経路が設計に組み込まれている
READMEはこの2点目を「reason こそが要点だ」と表現し、スコア列を意図的に持たないと明言している。理由は明快で、内部のモデルが持つ確信度は有料APIを叩くかどうかの支出ゲートとして使う値であり、「良いリードかどうか」を意味するように較正されていないからだ。較正されていない数値で閾値を切らせないために、そもそも列として出さない——という判断になっている。
必要なものは3つだけだ。LLMのAPIキー(OpenAI・Anthropic・OpenAI互換エンドポイントのいずれか)、データ提供元であるBetterContactのAPIキー、そして製品説明と対象市場の文章である。メールボックスの接続は不要で、これは後述する2026年8月の変更の直接的な結果だ。
reason 列書き出しの列は email, first_name, last_name, company, title, website, linkedin_url, reason, lead_id の9つ。READMEはこの列名について「これらは我々の列名ではなく、取り込む側の列名だ」と説明している。InstantlyとSmartleadが標準フィールドとして認識する名前に揃えることで、列マッピング無しで取り込めるようにする狙いだ。reason はどちらにとっても標準外なので、カスタム変数として渡り、メール本文のテンプレートに差し込める。
書き出しの挙動には非対称なルールが2つある。メールアドレスが解決できなかったリードも書き出される(アドレスは付加情報であって前提条件ではない)一方、判定で却下されたリードは決して書き出されない。却下にはキャンペーン単位の「条件に合わない」判定と、アカウント単位の恒久的なオプトアウトの2種類があり、どちらも常に除外される。
2026年8月の「finder cut」——送信機能が消えた差分を数える
このリポジトリを説明するうえで最も重要な事実は、記事更新時点のわずか2日前に起きている。
pre-finder-cut と main の差分を git diff --shortstat で計測した実測値作者は送信機能を切り落とす直前の状態に pre-finder-cut というタグを打っている。このタグと main を比較すると、192ファイルが変更され、3,225行が追加され、11,705行が削除されている。削除されたファイルは129個で、内訳の上位は openoutreach/emails が29ファイル、openoutreach/chat が10ファイル、openoutreach/legacy が15ファイル、テスト側の tests/emails が14ファイルだった。
コミットメッセージは何が起きたかをそのまま述べている。2026年8月19日に「送信レッグを削除する:emails/* を OpenEmailSequence へ移し、状態機械は RESOLVED で終わる」というコミットが入り、同日に「発見と判定を送信枠の残量でゲートするのをやめる:メールボックスがあろうと無かろうとファインダーはリードを見つける」というコミットが先行している。翌8月20日には「ツールをCLIファーストにする:PyPI向けにパッケージ化し openoutreach コンソールスクリプトを用意する」が入った。
分離先の OpenEmailSequence は2026年8月15日に作成されたリポジトリで、Django向けのメールドリップ配信ライブラリとして公開されている。つまり「送信は専門領域だから切り出す」という判断を、リポジトリの新設からわずか4日で実行に移したことになる。
旧バージョンを動かしたままの環境で確認すべきこと
READMEは削除した資金調達の仕組みを自ら開示している。旧バージョンにはOpenOutreach自身の宣伝キャンペーンが含まれ、利用者自身のメールボックスから利用者の名前で送信されていた。さらに送信するすべてのメールに「Sent with OpenOutreach」の一文が付与されていた。どちらも送信機能の削除と同時に消えている。
コミット履歴を追うと、この仕組みは2026年8月11日時点で「既定で有効な状態で出荷され、ローテーションし、割合の設定値を持たない」と訂正され、同日に「宣伝キャンペーンのオプトアウトをセルフホスト設置に限定する」というコミットが入っている。8月19日に「宣伝キャンペーンとその適格判定を削除する:これは我々の広告を運用者の身元で送るために存在していた」というコミットで撤去された。
現在の資金調達はデータ提供元へのアフィリエイトリンクのみで、利用者側に上乗せは無いとREADMEは説明している。8月19日より前のバージョンを稼働させたままの環境は、いまも自分の名義で当該ソフトウェアの宣伝メールを送りうるため、旧版を運用しているなら更新するか、少なくとも送信ログを確認する価値がある。
仕組み——発見から書き出しまでのループ
デーモンは短いサイクルを回し、その都度「いま何が必要か」を案件に尋ねる。キューテーブルは持たず、事前にスケジュールされるものも無い。1回のパスで順序付きのリストを1本だけ走査し、実行できる最初の1件で止まる——つまり優先順位はリストの順序そのものである。
アドレス解決はあるか"} C["結果を回収
命中=RESOLVED / 不発=NO_EMAIL"] D{"確信度の高い
判定済みリードはあるか"} E["READY_TO_FIND_EMAIL へ昇格"] F{"アドレスを
買う対象はあるか"} G["キャッシュ命中なら即解決
外れなら有料ジョブを発行"] H["候補を追加で発見し判定する"] A --> B B -- "ある" --> C B -- "ない" --> D D -- "ある" --> E D -- "ない" --> F F -- "ある" --> G F -- "ない" --> H C --> Z["このサイクルは終了"] E --> Z G --> Z H --> Z
課金が発生するのは3番目のステップだけで、そのゲートは「データ提供元を設定したかどうか」に尽きる。2番目と4番目に制限を設けないのは意図的で、インデックスの検索は無料、判定は利用者自身のLLMキーに対する1回の呼び出しなので、配給制にする対象が無いからだとREADMEは説明している。1サイクルで1つのことしかしない、という設計自体が上限になっている。
見落とされやすいのが、「発見」と「判定」でまったく違う仕組みが動いている点だ。両方を一括りに「AIが探す」と要約すると誤読になる。
・発見(discovery)はモデルを使わない:最初にLLMがキャンペーンの文章を検索キーワードへ変換するが、そこから先は数えるだけである。採用されたプロフィールに出現した単語を数え、語彙を1語ずつ足しながら、採用数の多かった方向へ次の問い合わせを割り当てる。READMEはこれを「モデルなし、頻度の調整つまみもなし」と表現する
・判定(qualification)はLLMとガウス過程の組み合わせ:適合の可否は常にLLMが決め、ガウス過程は「次に誰を評価するか」と「有料照会へ昇格させるか」だけを担当する
つまり候補の広げ方は素朴なカウントベースの探索で、機械学習が効いているのは評価順序と課金ゲートに限られる。--log-level debug を付けると、この探索の内部——フロンティア、各ノードのカウントと抽選、なぜそのノードを展開した/しなかったか、提供元の生の応答——が出力されるとヘルプに記載されている。
判定ループの内側はもう少し込み入っている。発見されたプロフィールは、提供元から返る企業属性のテキストからFastEmbedで384次元のベクトルに埋め込まれる。次にどのプロフィールを評価するかは、正例と負例のバランスで決まる。
・負例が正例を上回っているときは「活用」に倒し、適合確率の予測値が最も高いプロフィールを選ぶ(見込みの高い正例でパイプラインを埋める)
・それ以外のときは「探索」に倒し、BALD(Bayesian Active Learning by Disagreement)スコアが最も高いプロフィールを選ぶ(最も情報量の多いラベルを取りにいく)
適合の判定そのものは常にLLMが行い、ガウス過程モデルは「次に誰を評価するか」の選択と、QUALIFIED から READY_TO_FIND_EMAIL への昇格ゲートを担当する。学習に使われるのはLLMの適合判定だけで、送信の結果がこのループに入ったことは一度も無い——だから送信機能を手放してもモデルは何も失わない、という論理でREADMEは分離を正当化している。
新規キャンペーンには適合の根拠になる実績がまだ無いので、理想顧客像は数件の合成プロフィールとして書き出され、モデルの正例として埋め込まれる。実際の採用が1件出るたびに合成プロフィールが1件退役する仕組みで、でっち上げた根拠は実データが置き換わる速度で薄まっていく。
なお作者は学習ループについて、「これは進行中の実験であり、ランダムに選ぶより優れていると示されたわけではなく、そうだとも主張しない」とREADMEに明記している。ベイズ最適化を売り文句にしていないという点は、このツールを評価するうえで押さえておきたい。
案件が取りうる状態と、有料APIを叩く条件
案件(Deal)の状態遷移はドキュメント docs/profile_lifecycle.md に定義されている。運用時に見ることになる主要な状態は次のとおり。
| 状態 | 意味 | 課金 |
|---|---|---|
QUALIFIED |
LLMが適合と判定。この時点で既に書き出し対象 | なし |
READY_TO_FIND_EMAIL |
確信度ゲートを通過し、アドレス解決の待機列に入った | なし |
FINDING_EMAIL |
有料の照会ジョブが飛んでいる最中 | 発生 |
RESOLVED |
アドレスが取得できた。充足した案件の終着点 | 発生済み |
NO_EMAIL_BETTERCONTACT |
アドレスが見つからなかった。結果欄は空のまま | 命中しなければ課金なし |
FAILED |
LLMが不適合と判定(キャンペーン単位) | なし |
ここで押さえておきたいのが2点ある。ひとつはQUALIFIED の時点ですでに書き出せることで、メールアドレスはあくまで上乗せの付加情報であり前提条件ではない。もうひとつは昇格ゲートの具体的な閾値で、ガウス過程の事後分布が P(f>0.5) > 0.9 を満たしたときにだけ QUALIFIED から READY_TO_FIND_EMAIL へ進む。有料の照会を配給制にしているのはこの数値である。照会が実行できなかった場合は READY_TO_FIND_EMAIL に戻り、クレジットは消費されない。
もう一点、導入判断に関わる仕様がある。アドレス解決はまず運用者をまたいだ共有の連絡先キャッシュ(hub)に問い合わせ、命中すれば無料で RESOLVED に進む。外れたときだけ有料ジョブが飛び、解決したアドレスは hub へ戻される。無料で済む経路が用意されている代わりに、解決結果が自分の設置の外側に共有される構造になっているということなので、扱う対象によっては法的通知の該当箇所を読んでおく必要がある。
キューテーブルを持たずにデータベースの状態から次の仕事を決める設計は、Postgres LLMで「INSERTするだけでLLM処理」を実現するトリガー型OSS が採るトリガー駆動の考え方と対照的だ。あちらが書き込みイベントを起点にするのに対し、OpenOutreachは案件の現在状態を毎サイクル読み直す。
インストールと使い方——uvx openoutreachが通らない現状と動く手順
READMEのQuick StartとDockerドキュメントは、uvx openoutreach(または pip install openoutreach)を「サポートされるインストール方法」と明記している。Dockerのページには「これはインストール手順ではない」という但し書きまで置かれ、コンテナはサーバーで無人稼働させるための手段だと位置づけられている。
しかし2026-08-21 JST時点で、その推奨手順は通らない。
# READMEが推奨する手順(2026-08-21 JST 実測)
uvx openoutreach
# × No solution found when resolving tool dependencies:
# ╰─▶ Because openoutreach was not found in the package registry and you
# require openoutreach, we can conclude that your requirements are
# unsatisfiable.
配布インデックスを直接引くと https://pypi.org/simple/openoutreach/ は404を返す。存在しないパッケージ名を対照として同じ経路で引いても404、実在する requests は200なので、この404は「未公開」を意味する。python3 -m pip download openoutreach --no-deps も No matching distribution found で終わる。一方 https://pypi.org/project/openoutreach/ は200を返すが、これは存在しないパッケージ名でも200を返す経路なので、公開の証拠にはならない。
PyPI向けのパッケージ化コミットが入ったのは前日の2026年8月20日なので、これは近日中に解消される可能性が高い。導入を試すときは、まず pip install openoutreach を叩いてみて、失敗したら以下に切り替えるのが早い。
# 実際に通る手順:ソースから導入する
git clone https://github.com/eracle/OpenOutreach.git
cd OpenOutreach
make setup # uv pip install -e ".[dev]" と migrate をまとめて実行
make setup は install(uv pip install -e ".[dev]")に依存し、続けて python manage.py migrate --no-input を走らせる。Python 3.12.9の仮想環境で make setup をそのまま実行したところ、開発用の追加依存を含めて解決が通り、マイグレーションまで終了コード0で完了した。要求されるPythonのバージョンは pyproject.toml では3.11以上、READMEの本文では3.12以上と書かれており、両者に食い違いがある。
サーバーで動かす場合のコンテナは実在する。GitHub Container Registryの eracle/openoutreach は latest タグのマニフェストが200で取得でき、Dockerドキュメントはこのイメージを「ブラウザもVNCも入っていない軽量なPythonランタイム」と説明している。
# サーバーで無人稼働させる場合(docs/docker.md の記載どおり)
docker run --pull always -it -v ~/.openoutreach/data:/app/data ghcr.io/eracle/openoutreach:latest
-it が要るのは初回の対話オンボーディングのときだけで、環境変数で設定すればTTYは不要になる。注目したいのは公開するポートが1つも無いことで、ドキュメントは「公開すべきポートは無い——デーモンは自前のWebサーバーを持たず、覗くべきブラウザも無い」と明記している。この一文は後述するデモGIFの内容と正面から食い違う。
CLIの動作を実測する
導入後にまず叩くことになるのが status だ。初期化前の状態で実行すると、Djangoのトレースバックではなく型付きのエラーが返る。
openoutreach status
# error: not_initialized: no pipeline yet at <path>/data/db.sqlite3 — run `openoutreach` once to create it
# 終了コード: 1
make setup を通した後に --json を付けると、標準出力に構造化された状態が返り、終了コードは0になる。エラー時は標準出力が空で、メッセージは標準エラーに出る——「標準出力は結果だけ、ログは標準エラー」という出力契約が実際に守られていることが確認できる。スクリプトやエージェントから叩く前提の設計だ。
openoutreach status --json
初期化直後の応答からは、設定が必要な環境変数がそのまま読み取れる。onboarding.missing は4グループに分かれ、campaign(製品説明・対象市場)、llm(モデル名・APIキー)、bettercontact(APIキー)、account(運用者のメールアドレス・国・規約への同意)の計8項目が並ぶ。blocked には onboarding_incomplete と、「BetterContactのキーが無いため発見とアドレス解決の両方が止まっている」という no_credential の2件が入っていた。totals には leads_seen から exportable_without_email まで10個のカウンタが用意されている。
用意されている動詞はソースのドキュメンテーション文字列に列挙されている。
| コマンド | 役割 |
|---|---|
openoutreach |
そのまま実行(初回はオンボーディングが走る) |
openoutreach run |
上と同じものに名前を付けたもの |
openoutreach status [--json] |
設定・停止理由・カウント・次の一手 |
openoutreach reset [--campaign N] [--all] |
キャンペーンの探索をやり直す |
openoutreach export_leads --campaign N |
CSVの書き出し |
加えてDjango本体のコマンド(migrate / runserver / createsuperuser)もそのまま使える。CRMの実体はDjango Adminで、make admin で起動すると http://localhost:8000/admin/ からリード・企業・案件を閲覧でき、判定の1件1件が理由付きの行として読める。動作設定もこの画面から行う構成で、READMEは全体設定を SiteConfig、キャンペーン単位の設定を Campaign のモデルで変更すると案内している。専用の設定ファイルを別に持たない分、閲覧と設定変更が同じ画面に集約される。
データベースの既定パスは ~/.openoutreach/data/db.sqlite3 だが、チェックアウト内で実行した検証ではリポジトリ配下の data/db.sqlite3 が使われた。--db PATH オプションと OPENOUTREACH_DB 環境変数のどちらでも変更できる。
openoutreach export_leads --campaign "My Campaign" > leads.csv
LinkedInスクレイパーだった過去と、いまも残るlinkedin_url列
このリポジトリの説明が混乱しがちな理由は、履歴をたどると分かる。作成日は2016年10月7日で、当時の名前は eracle/linkedin だった。旧URLは現在もOpenOutreachへ301リダイレクトする。2017年10月には「自分用のheadless Chromeを追加した」というコミットが入っており、ブラウザ操作でLinkedInを収集するScrapyベースのスパイダーだった時期があることが履歴から読める。
現在のREADMEの先頭には依然としてデモGIFが貼られているが、このファイルの最終更新は2026年4月24日、つまり8月の変更より4か月前だ。1フレーム目には docker run --pull always -it -p 5900:5900 -p 6080:6080 ... という起動コマンドが映っている。5900はVNC、6080はnoVNCの標準ポートで、ブラウザ操作を画面越しに覗く構成を意味する。ところが現在の docs/docker.md は、同じイメージを「ブラウザもVNCも入っていない軽量なPythonランタイム」と説明し、起動例からはポート公開のオプションが消え、「公開すべきポートは無い」と本文で断言している。看板のデモ映像と本文の説明が正面から食い違っている状態なので、GIFを根拠に現在の構成を判断しないほうがよい。
ここで正確を期したいのが、LinkedInとの関係だ。「LinkedInを使わなくなった」と要約すると不正確になる。
・スクレイピングはしない:現行READMEは「プラットフォーム利用規約に触れる面がゼロ——ブラウザレス、ソーシャルネットワークのアカウント無し、スクレイピング無し。BANされるアカウントがそもそも存在しない」と述べる。pyproject.toml の依存にScrapy・Selenium・Playwrightは含まれない
・linkedin_url という列は残っている:書き出しのコードでは lead.profile_url の値が linkedin_url 列にマッピングされる。この値はライセンス契約したデータ提供元のペイロードに含まれるプロフィールURLであって、収集した結果ではない
・アドレス解決の問い合わせにも使われる:提供元クライアントは照会時に linkedin_url をキーの1つとして渡し、コード内のコメントは「linkedin_url 単体でも動くが、氏名と会社名があると命中率が上がる」と説明している
つまり「LinkedInのURLを扱う」ことと「LinkedInをスクレイピングする」ことは別で、現在のOpenOutreachは前者だけを行う。この区別は、導入可否をコンプライアンス面で判断するときに最初に確認されるポイントになる。
収集そのものを目的とするツールとは設計思想が正反対なので、比較すると差が分かりやすい。対象サイトを直接クロールする Spider Rs:Rust製の高速Webクローラーで大規模サイトマッピングを実現 や、特定プラットフォームからの収集に特化した reddit-universal-scraper:Redditのあらゆるコンテンツを自動収集するPythonツール完全ガイド は、取得の主体が自分側にある。OpenOutreachは取得を提供元に委ね、自分側は判定だけを行う構成を選んでいる。
類似ツールとの比較と、導入前に確認すること
現行のOpenOutreachがどの位置にいるのかを、旧版および一般的なカテゴリと並べて整理する。
| 観点 | OpenOutreach(現行・8/19以降) | OpenOutreach(旧・8/18まで) | 一般的なリードDB/配信SaaS |
|---|---|---|---|
| 入力 | 製品説明の文章 | 製品説明の文章 | 検索条件、または持ち込みリスト |
| 候補の取得元 | ライセンス提供元(BetterContact Lead Finder) | 同左+送信機能 | 自社データベース |
| 出力 | 理由文つきCSV 1本 | CSV+自動送信 | 行データの一覧・配信結果 |
| メール送信 | 行わない | 利用者のメールボックスから送信 | 製品による |
| 選定理由の開示 | reason 列に文章で出力 |
同左 | 通常は非開示 |
| 稼働場所 | 自分の端末・サーバー | 同左 | ベンダーのクラウド |
| ライセンス | GPLv3 | GPLv3 | 商用 |
READMEは「あなたのためにリードを判定してくれる同等のツールは、いずれも有料SaaSだ」と自らの位置づけを説明している。判定の根拠が読める点と、稼働場所が手元である点が、このツールが主張する差分ということになる。
導入前に確認しておきたい点を、実測とドキュメントから整理する。
・外部サービスへの依存は2つ:LLMのAPIキーとBetterContactのAPIキーが要る。BetterContactは発見と解決の両方を担うため必須で、READMEは「障壁は請求ではなくアカウント」と表現している(検索自体は無料で、解決時のみ課金)
・アフィリエイトリンクが導線に含まれる:オンボーディング中に提示されるデータ提供元へのリンクはアフィリエイトリンクで、経由するとプロジェクトに手数料が入る。READMEはこれを明示し、別経路での登録も可能だとしている
・取り込み側の重複排除は自分で有効にする:書き出しは一方向の境界として設計されており、送信側の抑止リストやオプトアウトの義務は送信する側が持つ。同じリードを2回書き出すと2回接触しうるため、READMEはSmartleadでのオプトイン設定とInstantlyでの未文書化の挙動に注意を促している
・データ保護法上の立場:法的通知(LEGAL_NOTICE.md)は、接続する第三者サービス、データ管理者としての利用者の責任、オプトインが前提でない法域での自動的なニュースレター登録、中央の連絡先ストアなどを扱うとREADMEは説明している。実運用の前に原文を確認する対象になる
・学習ループは実験段階:前述のとおり、作者自身がランダム選択に対する優位を主張していない
判定の質を決めるのは製品説明の記述精度なので、最初に投入する文章を具体的に書くことが実質的な設定作業になる。「SaaS企業の営業責任者」より「シリーズBのスタートアップでエンジニアリング組織を見ている責任者」のほうが、LLMが生成する検索語も適合判定も安定する。判定がずれていたら、モデルの設定ではなく製品説明の文章を直す——という修正経路がこのツールの設計思想である。