この記事は Zenn に投稿したものの再掲です。
https://zenn.dev/reona777/articles/salesforce-fields-exist-vs-usable
教育系の事業所で現場の運営をしています。エンジニアではありません。2026年の春から Claude Code で社内ツールを作り始めて、5か月で6本を本番稼働させました。
前の記事では、自動化が無言で失敗する話を書きました。
ここまでの6本は Claude Code そのものの話でした。今回は道具ではなく、触った相手の話です。作ったツールのうち4本が Salesforce を読み書きしていて、そのたびに同じ種類の躓き方をしました。
先に前提を書きます。私はその組織の管理者ではありません。業種別の管理パッケージが載った既存の環境に、一利用者として毎日ログインしていて、そこへ外から API で読み書きしに行きました。画面は毎日見ています。つまり**「何がどこにあるか知っているつもり」の状態から始めた**わけで、今回の6つはほぼ全部、そのつもりが外れた話です。
結論から書きます。項目が「ある」ことと「使える」ことは、まったく別でした。 そして6つとも、スキーマを読んだだけでは分からず、数えて初めて分かりました。
以下、オブジェクト名は伏せて Lesson__c(予定)、Session__c(予定にぶら下がる対象者の記録)、Contact(人)と呼びます。実際は業種別パッケージなので接頭辞が付きますが、話の中身は変わりません。
1. 表示名が同じ項目が2つあって、選択肢が違った
最初に踏んだのがこれです。Contact にある「文理」という項目を更新したくて、describe で項目一覧を取ったら、「文理」が2つ出てきました。
humanities_sciences_cd_t__c ラベル「文理(担当者)」 選択肢: 文系 / 理系
Humanities_Science_Code__c ラベル「文理」 選択肢: 1 / 2 / 3
画面で編集していたのは前者です。後者は画面に出ておらず、値の体系がそもそも違いました。API名から中身は推測できません。 Humanities_Science_Code__c のほうがそれらしい名前をしているぶん、むしろ危ない。
Code のほうへ「文系」と書き込もうとすれば選択肢違いで弾かれるので気づけますが、もし両方が同じ選択肢を持っていたら、間違ったほうを黙って更新し続けていたはずです。
やったこと: 更新対象を決めるときは、ラベルではなく API名と選択肢の実際の値まで describe で確認してから書く。似た名前が複数出たら、片方は使わないと決めてコメントに理由を残す。
2. 項目はあるのに、7,000件あまり全部が空だった
次がこれです。Session__c に Student__c という Contact への参照項目があり、当然これで人に辿れると思いました。設計上もそこを入口にするつもりでした。
念のため数えたら、7,000件あまり、全部空でした。
SELECT COUNT(Id) FROM Session__c WHERE Student__c != null
-- 0
参照項目は定義されていて、画面にも項目として存在します。ただ誰も入れていない。運用がその項目を使っていないだけで、スキーマとしては何も間違っていません。
同じことが他にもありました。人のレコードにある拠点の項目も全件空で、絞り込みに使えません。
ここが今回いちばん効いた教訓です。 スキーマは「入れられるもの」を定義しているだけで、「入っているもの」は一切表していません。設計の前に数える。 これをやらないと、動くけれど何も返さないクエリを本番に置くことになります。前の記事に書いた「0件で正常終了」が、いちばん出やすい形で待っています。
3. 親から本体に辿り着けなかった
予定から対象者を引きたい、という要求は自然です。ところが Lesson__c にも、その親のテンプレートにあたるオブジェクトにも、人への参照がありません。
辿れるのは子の Session__c だけで、そこに名前が文字列で入っています。参照ではなく文字列です。
しかも、その子が全件には付いていませんでした。
対象期間の Lesson__c 5,344件
うち Session__c があるもの 5,217件
差の127件の内訳 下書き100 / 公開26 / 完了1
差の127件のうち100件は、そもそも除外対象の種別だったので問題ありません。困るのは公開状態の26件で、これは実在の予定でした。 つまり「子から辿る」を主経路にすると、26件が黙って落ちます。
やったこと: 主経路は別に持ち、子は補完としてだけ使う。しかも補完で得た名前は「推測」として扱い、名簿と完全一致したときだけ採用する。一致しなければ処理せず通知に回します。ここは前の記事の結論と同じで、別人に届くほうが害が大きいからです。
4. 同じ名前の項目が、親では日付・子では文字列だった
Lesson__c の日付項目は date 型です。なので子でも同じつもりで絞り込んだら、こうなりました。
MALFORMED_QUERY: value of filter criterion for field 'Lesson_Date__c'
must be of type string
子側の同名項目は文字列型でした。 日付として比較できません。同じ名前で、意味も同じで、型だけ違います。パッケージ側が転記のときに文字列で持たせているようです。
回避は親経由での絞り込みでした。
SELECT Id, Student_Name__c
FROM Session__c
WHERE Lesson__r.Lesson_Date__c >= 2026-07-01
やったこと: 子オブジェクトを直接絞りたくなったら、まず親から辿れないかを見る。エラーメッセージが親切だったので気づけましたが、これが文字列比較として通ってしまう条件だったら、また無言で0件です。
5. 同じ人が、複数の表記で存在していた
これは結局いちばん深く刺さりました。
予定の名前に埋め込まれている表記と、Contact の正式表記が違う人がいます。 姓名の間の空白なら正規化で吸収できます。実際 NFKC と空白除去は入れていました。吸収できなかったのは異体字です。
片方が常用漢字で、もう片方が異体字でした。「高」と「髙」、「辺」と「邊」のような関係です。字が1文字違うだけで、読みも本人も同じです。
同じ人です。人間が見れば分かります。プログラムから見れば別人で、しかもエラーは出ません。名寄せに失敗して、その人だけ通知が飛ばない。
担当者側でも似たことがあり、同一人物が姓名の間の空白の有無で分裂していて、集計すると2人分になっていました。こちらは正規化で吸収できる側です。
やったこと: 名前を突合キーにするのをやめる方向に寄せる。どうしても名前でしか繋がらない箇所は、どちら側の表記を正とするかを決めて固定しました。今回は「予定の名前と通知先の名簿」を正にしています。Contact 側を正にすると、正しいはずの表記を使ったせいで届かなくなる人が出るからです。
正しいデータを使うと壊れるという状況は、事前には想像できませんでした。
6. 権限が、データ設計の前提そのものを変えた
「いつ誰がこの項目を変えたか」を追いたくなり、履歴を見に行きました。
Contact の履歴に残っていたのは5項目だけで、私が追いたかった項目は入っていません。一方で予定オブジェクトのほうは手厚く、担当者の付け替えが13,000件超も残っていました。オブジェクトごとに追跡設定が違うわけです。
では追跡をONにすればいい、と思って設定画面を開くと、見えるのに保存できません。 接続に使っているユーザーのプロファイルにはアプリケーションのカスタマイズ権限が無く、設定は閲覧だけ許可されていました。権限を持つ管理者への依頼が必要で、そこまでする話ではないと判断して諦めました。
代わりの手段も、期待したほど残っていませんでした。
LastModifiedDate は最後の1回しか持ちません。誰かが後から直すと、それ以前は消えます。さらに、子レコードを作っても親の LastModifiedDate は動きません。
そして1つ、明確に読み間違えたものがあります。updated() を「その期間に更新されたレコード」だと思って使いましたが、これは違います。**「現在の最終更新日時がその期間にあるレコード」**しか返しません。あとから更新されたレコードは、過去の期間を指定しても出てきません。変更履歴の代わりにはなりません。
結局、証拠になったのは業務のデータ側でした。 過去に一括更新スクリプトを流したときの undo 用 JSON が残っていて、そのスクリプトは在職中の人だけを対象にする作りだったので、**「そのファイルに載っている=実行時点で在職扱いだった」**と読めます。履歴機能ではなく、自分が残した副産物のほうが証拠になりました。
設計の前に投げるようにした4本
6つとも共通して、スキーマを読んだ段階では気づけず、数えたら分かりました。 なので、SOQL を書く前に必ずこれを投げるようにしています。
その項目に値が入っているか
SELECT COUNT(Id) FROM 対象 WHERE 項目 != null
ゼロなら、その項目は存在しないものとして設計する。
参照や子レコードが全件に付いているか
SELECT COUNT(Id) FROM 親
SELECT COUNT(Id) FROM 子
差があるなら、差の中身の内訳まで見る。「ほぼ全部」は主経路にできません。
選択肢の実際の値
describe の picklist の値をそのまま見る。画面のラベルではなく、保存されている値です。
突合キーの重複と表記ゆれ
SELECT 名前, COUNT(Id) FROM 対象 GROUP BY 名前 HAVING COUNT(Id) > 1
ここに何か出たら、名前をキーにするのは諦めます。
AIにSOQLを書かせるときに足りないもの
シリーズで一貫して書いていることが、ここでも同じ形で出ました。
AIに SOQL を書かせると、きれいに通るクエリが出てきます。 存在する項目名を使うので構文エラーになりません。参照項目があれば当然のように辿ってくれます。
ただし、その項目が全件空であることは、メタデータのどこにも書いていません。 型もラベルも参照先も定義されていて、定義上は完璧に使えます。使えないのは運用の結果であって、スキーマの外にある情報です。
なので、渡すものをスキーマから「数えた結果」に変えました。
この項目は7,000件あって全部 null なので使えない。人に辿るには子の名前(文字列)経由しかなくて、その子は127件欠けている。欠けた中に実在のものが26件あるので、主経路にはしないで。
こう伝えると、最初から補完扱いの設計が出てきます。AIが持っていないのは構文ではなく、その組織で実際にどう運用されているかでした。 そしてそれは、自分で数えるしか手に入れる方法がありません。
最後に
Salesforce を外から触り始めたときに一番効くと思っていたのは SOQL の書き方でしたが、実際に効いたのは数える習慣のほうでした。
画面を毎日見ているぶん「知っているつもり」が強く、それが全部の躓きの入口になっています。画面に見えているのは、権限と運用でフィルタされた後の姿で、API から見えるものとは違いました。
これから Salesforce まわりの実務を増やしていくので、この手の話は続けて書きます。
ほかに作ったものは GitHub に置いています。
書いている人
エンジニアではありません。勤務先の業務ツールを Claude Code で作って運用していて、作ったものと、壊れたときに直した話を書いています。
Claude Code の実務運用については X(@KouritsuONI)でも書いています。スプレッドシート・GAS・LINE・Salesforce まわりの業務自動化について、ご相談は X のDMからどうぞ。
この記事のシリーズ
Claude Code の実務運用について、順に9本書いています。
- 非エンジニアがClaude Codeで社内ツールを6本 本番稼働させるまでにやったこと
- AIに作らせたツールが本番で壊れた5つの原因と、直し方
- Claude CodeにCONTEXT.mdとSPEC.mdとADRを書かせると、途中で破綻しなくなる
- Claude Codeのスラッシュコマンドには、手順ではなく踏んだ罠を書く
- Claude Codeのメモリに53本ためて分かった、書く価値のある事実とない事実
- 実行は成功、でも誰にも届いていない。無言で失敗する自動化に気づく仕掛け
- Salesforceの項目は「ある」と「使える」が別だった。外から自動化して踏んだ6つ(この記事)
- Salesforceのパスワードを変えたら、連携が8箇所いっせいに止まった
- Salesforceのログインが2027年6月に廃止される。自動化10本を調べたら、直す場所は1つではなかった