
【Dify×Salesforce×Claude】PDF転記作業を卒業!PDF解析からSalesforce登録まで自動化してみた
皆さんこんにちは!
PDFからの情報コピペ作業、もううんざりしていませんか?職務経歴書から候補者情報を転記したり、請求書の内容をシステムに入力したり...単純作業に時間を費やしている方も多いのではないでしょうか。
採用・営業・法務・経理など、どの部署でも共通して抱えているこの課題。PDFの情報を手作業でCRMや管理システムに転記する作業は、時間がかかる上にミスも起こりやすいですよね。
そこで今回は、Difyのチャットフロー機能を使って、この面倒な作業を完全自動化するアプリを作ってみました!PDFをアップロードしてチャットで「情報を抽出して」と話しかけるだけで、必要な情報を綺麗に整理してくれます。さらに「Salesforceに登録して」と言えば、自動でリードレコードまで作成してくれます!
このアプリがあれば、従来30分かかっていた作業が3分で完了するのも夢じゃないかも!
今回はこのアプリの仕組みや構築方法を詳しく解説していきます!
最後にはDMLファイルもご用意しています!
今回の担当

完成イメージ
今回例として作成するのは、職務経歴書ファイルを元に求職者情報を抽出し、Salesforceのリードオブジェクトにレコードを登録するアプリケーションです!
以下のような流れを想定しています!

1. ユーザーがPDFファイルをアップロードし「求職者の情報を抽出して」と入力
2. システムがPDFを解析し、名前・職務経歴・スキル・自己PRを構造化データとして抽出・出力
3. 抽出された情報をユーザーが確認し、「この情報を元にSalesforceに登録して」と入力
4. システムがSalesforce APIを通じて自動的にリードレコードを作成
アプリの全体像はこちらです!

アーキテクチャ概要
システム構成は以下の通りです!
抽出した情報を登録する先をSalesforceにしていますが、API連携できるものであれば、他サービスへの連携にも応用可能です!
Dify: チャットフロー全体の制御とユーザーインターフェース
Claude Sonnet 4: PDF内容の解析とパラメータ抽出
Python: 構造化データへの変換
Salesforce REST API: リードレコードの作成
Difyチャットフローの実装
今回は抽出した内容を確認したり必要に応じて編集してからSalesforceに登録したかったので、やり取りが可能なチャットフローを選んでいます!
また、ユーザーが求めていることに応じてフローを分岐し、PDFの解析・Salesforceへの登録の処理を分けるよう設計しています。

では、テンプレートから作成>チャットフローを選択し作成していきましょう!
開始
開始の入力フィールドを追加します。フィールドタイプは単一ファイルにし、ファイルタイプはドキュメントと画像を選択しておきます。(PDFとPNGで動作確認済みです!他はサポートされていない場合がありますのでご了承ください)
※「アップロードされたファイルのタイプ」は、ローカルがおすすめです。
URLだと取り込みエラーになる場合があります。

質問分類器
開始した時のチャット内容からユーザの目的を判断し、これによってフローを分岐させます!

クラス1に当てはまる場合は求職者情報の抽出、クラス2に当てはまる場合はSalesforceへの登録というように分岐させます。
ここからは、まず求職者情報の抽出機能、その後Salesforceへの登録機能についてご説明します!
実装🔧 求職者情報の抽出機能
IF/ELSE
IF/ELSEにて添付ファイルがあるかを確認し、ない場合は「ファイルを送信してください」というメッセージを返します。

LLM
ファイルが見つかった場合は、Claude Sonnet4.0で文字起こししてもらいます!

※今回はLLMノードでClaudeを使用していますが、Claudeが使用できない場合は「テキスト抽出ノード」でPDFやtxtファイルの文字起こしが可能です!(詳細はDify ドキュメントをご参照ください。)
パラメータ抽出
文字起こしした内容から、欲しい情報を抽出するための設定をします!
今回は氏名・自己PR・スキル・職務経歴を取得したいため、
パラメータ抽出ノードにて、name・selfPromotion・skill・workExperienceという4つのパラメータを設定しました。

コード実行
抽出した情報をわかりやすく出力し、次のSalesforce登録へ使用できるようにするため、コード実行ノードでJSON形式にフォーマットしておきます。

使用したPythonコードはこちらです!
import json
def main(name: str, selfPromotion: str, skill: str, workExperience: str) -> dict:
inner_dict = {
"name": name,
"selfPromotion": selfPromotion,
"skill": skill,
"workExperience": workExperience
}
return {
"result": json.dumps(inner_dict, ensure_ascii=False)
}LLM
JSONに変換したものをそのまま出力しても良いのですが、読みにくいので、人間が読みやすい形に変換してもらいます!
ここのモデルは何でもOKです!

回答
最後に、情報を読みやすく整えたものを回答して終了です!

おまけ(会話の開始 を設定)
ユーザーに使い方を説明したいので、会話開始時にボットからユーザーに最初に送信されるメッセージを設定しておきます!

以下のように設定しておきました!

実行🔍 求職者情報の抽出機能
求職者情報の抽出機能が完成したので、プレビューで動かしてみます!
画面右上のプレビューボタンをクリックすると、ファイルアップロード画面と、先ほど設定したメッセージが表示されることが確認できます!

では実際に、PDFをアップロードして試してみます!
今回はこちらのお試し用PDFを使用しています。
このように出力されました!
PDFから、4つのパラメータ(氏名・自己PR・スキル・職務経歴)について出力してくれています!
ここまでのDMLファイルは、こちらからダウンロードいただけます!
実装🔧 Salesforceへの登録機能
では続いて、Salesforceへの登録機能を実装していきます!
まずは事前準備をしていきましょう!
ーーーーーーーーーーー事前準備ーーーーーーーーーーーーー
Salesforce側の準備 - 外部クライアントアプリケーション作成
Salesforceにて、外部クライアントアプリケーションを作成します!
設定 > 外部クライアントアプリケーションマネージャー > 新規外部クライアントアプリケーションボタンをクリック。
アプリケーション名と、取引先責任者メールアドレス(自分のアドレスでOK)を入力します!

「OAuthを有効化」にチェックを入れ、コールバックURLとOAuth範囲を選択します!コールバックURLは今回は使用しないので、適当なものでOKです!
また今回はクライアントログイン情報フローを使用したいので、こちらを有効化します!

ここまで設定できたら、作成ボタンをクリックします!
作成後、ポリシータブの編集ボタンをクリックし、クライアントログイン情報フローの実行者を設定します!
本番環境ではAPI専用ユーザーを作成するのがベストですが、今回はPlaygroundで実装しているので自身のユーザー名を入力しました。

保存後、設定タブのコンシューマー鍵と秘密をクリックしてコンシューマーキーとコンシューマーシークレットを取得します!

これらの値はDifyに設定するのでメモしておきます!

これで、SalesforceにAPIを叩いてレコードを作成する準備ができました!
また今回はリードオブジェクトを使用するので、自己PR・スキル・職務経歴のカスタム項目を作成しておきました!(割愛します)
ではDifyに戻ります!
環境変数の設定
先ほど取得したコンシューマーキーとコンシューマーシークレットをDifyに環境変数として設定します!
右上のENVボタン > 環境変数を追加をクリック。下記の3つを登録します!
CLIENT_IDにコンシューマーキー、CLIENT_SECRETにコンシューマーシークレットを設定。TOKEN_URLには、設定 > 私のドメイン で確認したURL に「/services/oauth2/token」を追加したURLを設定します。
https://〇〇.my.salesforce.com/services/oauth2/token のような形式です!

ーーーーーーーーーーー事前準備ここまでーーーーーーーーーーーーーーー
ここから、チャットフローの実装を進めていきます!
これから実装していくのは、質問分類器のクラス2に当てはまる場合の機能です!

パラメータ抽出
ここでは、ユーザーから登録したい求職者情報をメッセージで送信されることを想定しています。
先ほどの求職者情報抽出機能で出力した情報をコピペしてメッセージで送ってもらうイメージです。

IF/ELSE
全てのパラメータに値があるかを確認します!

パラメータが不足している場合は、情報が不足している旨のメッセージを送信して終了します。

回答

コード実行
全てのパラメータが揃っている場合、SalesforceのREST APIを使って、
リードオブジェクトにレコードを作成します!

コード内容はこちらです!
⚠️ 下記コードには、リードオブジェクトに作成したカスタム項目SelfPromotion__c、Skill__c、WorkExperience__cが含まれています。
ご自身の環境に合わせて変更してご使用ください☺️
import requests
import json
import os
# --- メイン関数 ---
def main(name: str, selfPromotion: str, skill: str, workExperience: str, CLIENT_ID: str, CLIENT_SECRET: str, TOKEN_URL: str) -> dict:
sobject_api_name = "Lead" # SObject名を指定
last_name_val = name
company_val = name
selfPromotion_val = selfPromotion
status_val = "Open - Not Contacted"
skill_val = skill
workExperience_val = workExperience
# Salesforceアクセストークン取得時に、main関数の引数として受け取った接続情報を使用
access_token, instance_url, token_error = _get_salesforce_access_token(
CLIENT_ID,
CLIENT_SECRET,
TOKEN_URL
)
if token_error:
current_result = {
"status": "error",
"message": f"Salesforceアクセストークン取得失敗: {token_error}",
"salesforce_response": None
}
return {"result": json.dumps(current_result, ensure_ascii=False)}
if not access_token or not instance_url: # instance_urlもチェック
current_result = {
"status": "error",
"message": "SalesforceアクセストークンまたはインスタンスURLが取得できませんでした。",
"salesforce_response": None
}
return {"result": json.dumps(current_result, ensure_ascii=False)}
record_payload = {
"LastName": last_name_val,
"Company": company_val,
"SelfPromotion__c": selfPromotion_val,
"Skill__c": skill_val,
"WorkExperience__c": workExperience_val,
}
if sobject_api_name == "Lead":
record_payload["Status"] = status_val
sf_response, creation_error = _create_salesforce_record(
access_token, instance_url, sobject_api_name, record_payload
)
current_result_dict = {}
if creation_error:
current_result_dict = {
"status": "error",
"message": f"Salesforceへのレコード作成失敗 ({sobject_api_name}): {creation_error}",
"salesforce_response": sf_response
}
elif sf_response and sf_response.get("success"):
record_id = sf_response.get("id")
record_url = None
if record_id:
# ユーザー指定のURL形式で構築
# 注意: ドメイン部分は固定です。instance_url から動的に取得する方がより堅牢ですが、
# ユーザーの指定に合わせています。
record_url = f"https://cunning-otter-muyevc-dev-ed.trailblaze.lightning.force.com/lightning/r/{sobject_api_name}/{record_id}/view"
current_result_dict = {
"status": "success",
"message": f"Salesforceにレコードが正常に作成されました (SObject: {sobject_api_name})。",
"salesforce_response": sf_response,
"record_url": record_url # 生成したURLを追加
}
else:
current_result_dict = {
"status": "error",
"message": f"Salesforceでのレコード作成処理中にエラーが発生しました (SObject: {sobject_api_name})。",
"salesforce_response": sf_response if sf_response else {"errors": ["Unknown error during record creation."]}
}
return {"result": json.dumps(current_result_dict, ensure_ascii=False)}
# --- ヘルパー関数 ---
def _get_salesforce_access_token(CLIENT_ID: str, CLIENT_SECRET: str, TOKEN_URL: str) -> tuple[str | None, str | None, str | None]:
payload = {
'grant_type': 'client_credentials',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
try:
response = requests.post(TOKEN_URL, headers=headers, data=payload, timeout=10)
response.raise_for_status()
token_data = response.json()
access_token = token_data.get('access_token')
instance_url = token_data.get('instance_url')
if not access_token or not instance_url:
return None, None, f"エラー: アクセストークンまたはインスタンスURLをレスポンスから取得できませんでした。 レスポンス: {token_data}"
return access_token, instance_url, None
except requests.exceptions.HTTPError as http_err:
return None, None, f"HTTPエラーが発生しました: {http_err} - レスポンスボディ: {getattr(http_err.response, 'text', 'N/A')}"
except requests.exceptions.RequestException as req_err:
return None, None, f"リクエストエラーが発生しました: {req_err}"
except json.JSONDecodeError:
return None, None, f"レスポンスのJSONデコードに失敗しました。レスポンス: {getattr(response, 'text', 'N/A')}"
def _create_salesforce_record(access_token: str, instance_url: str, sobject_name: str, record_data: dict) -> tuple[dict | None, str | None]:
api_endpoint = f"{instance_url}/services/data/v59.0/sobjects/{sobject_name}/"
headers = {'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json'}
try:
response = requests.post(api_endpoint, headers=headers, json=record_data, timeout=10)
response.raise_for_status()
return response.json(), None
except requests.exceptions.HTTPError as http_err:
error_msg = f"HTTPエラーが発生しました: {http_err} - レスポンスボディ: {getattr(http_err.response, 'text', 'N/A')}"
try:
error_details = http_err.response.json()
error_msg += f" Salesforceからのエラー詳細: {json.dumps(error_details, ensure_ascii=False)}"
except json.JSONDecodeError:
pass
return None, error_msg
except requests.exceptions.RequestException as req_err:
return None, f"リクエストエラーが発生しました: {req_err}"
パラメータ抽出
レコード作成はコード実行ノードで完了していますが、作成されたレコードのURLをチャットで回答させたいので、コード実行のresultからrecord_urlを取得します!

回答
下記のように設定して、作成したレコードURLを回答させます!

おまけ(会話の開始 を編集)
必要に応じて編集しておきます!
これでSalesforceへの登録機能が完成です!
実行🔍 Salesforceへの登録機能
では動作確認をしてみます!
プレビューから、「Salesforceへ登録して」という文言と一緒に、名前・自己PR・スキル・職務経歴を送信します!
Salesforceへレコードを作成し、URLを返すことができました!
今回はレコードの作成のみの機能にしていますが、外部キーを用意して更新機能も付けるとより実用的になりますね!
ここまでのDMLファイルは、こちらからダウンロードいただけます!
アプリケーションを公開
最後に公開して、Dify外でも実行できるようにします!

「アプリを実行」をクリックすると、下記のような画面からアプリを実行できます!URLを共有して使用することも可能です。

⚠️ 実際に運用する場合には、個人情報の取り扱いには十分ご注意ください
まとめ
今回は、PDFから情報の抽出+Salesforceへ連携するアプリケーションを作成してみました!
DifyでLLMの自然言語処理能力と外部システムのAPI連携を組み合わせることで、従来は複雑なプログラミングが必要だった業務自動化が、驚くほど簡単に実現できるようになります!
Difyに限った話ではないですが、重要なのは、完璧を求めすぎずに「まず動くものを作る」こと!そして実際に使いながら改善していくことです。
皆さんも、ぜひご自身の業務に合わせてカスタマイズして、日々の作業を効率化してみてくださいね!
SAMURAIでは、DifyやPythonについて学べるレッスンを提供しています。
初心者でもわかりやすく、必要な知識を段階的に習得できるので、是非チェックしてみてください!