複数組織のZoho CRM連携とは、別々に契約された2つのZoho CRMの間で、関数(Deluge)と接続(Connections)を使い、APIでレコードを送り合う仕組みです。グループ会社が会社ごとにCRMを持ちながら、見積や受注の情報を打ち直さずに受け渡したいときに使います。

当社は、中小の製造業(個別受注生産)で、この連携を構築しました。このお客様は、販売を担う会社と製造を担う会社に分かれ、それぞれがZoho CRMを持っています。販売を担う親会社のCRMから、製造を担う子会社のCRMへ見積や受注を送り、子会社側から外部の生産管理システムへ渡します。生産管理システムが返した納期回答や出荷・入庫の実績は、子会社側のCRMを経由して親会社のCRMへ「戻し」ます。

システムは受入テストの段階です。この記事では成果ではなく、本番切替の前後に見つかった4つのつまずきと、設計で防ぐ方法をまとめます。

この記事は、本番で2つの組織がつながったあとの話です。

Zoho CRMの接続(Connections)とは:2つの組織は接続でつなぐ

Zoho CRMの関数から別の組織のAPIを呼ぶときは、「接続」(設定の開発者向けメニューにあります)を使います。接続とは、どのサービスに・どの権限で・誰の認可でアクセスするかを保存したものです。関数の中では、invokeurl に接続名を渡すだけで、認可済みの組織のAPIを呼べます(公式ドキュメント「Connections | Help - Zoho Deluge」)。Delugeの書き方の基本は「はじめてのDeluge(デリュージ)」で紹介しています。

今回の構成では、各組織に次の2種類の接続がありました。

接続 向き先 使い道
自組織用の接続 自分の組織 自分のレコードの読み書き、通知メールの送信
相手組織用の接続 相手の組織 相手のレコードの作成・更新(送信と戻し)

子会社側のレコードには、どの親会社レコードから来たかを示す「連携元キー」を持たせました。PARENT:Quote_Items:<親会社のレコードID> のような形です。戻しの関数はこのキーから親会社のレコードIDを取り出し、相手組織用の接続で更新します。

つまずき1:相手組織用の接続が、自分の組織を向いていた

起きたこと

本番切替から約1週間後、親会社の担当者から、納期回答が子会社のCRMには届いているのに親会社のCRMに反映されない、という連絡がありました。調べると、送信も、生産管理システムからの回答の着地も、子会社側のワークフローの起動も正常でした。止まっていたのは、子会社から親会社への戻しだけです。

関数の実行ログには、次の行が出ていました。

【納期回答戻し】★中止 接続先組織が想定と異なります。期待: (親会社の先頭桁) / 実際: (子会社の先頭桁)

子会社側にある「親会社向けの接続」が、親会社ではなく子会社自身を向いていました。ログからは理由まで分かりませんが、認可したユーザーが両方の組織に所属しており、認可の画面で子会社の組織が選ばれたと当社は推定しています。

なぜ見落としたか

サンドボックスでは、この接続がどの組織を向いているかを実測していました。戻しの成功も確認済みです。ところが本番切替の日は、接続が「存在する」「有効である」ことの確認で終えていました。本番で戻しを1件通す確認もしていません。接続の画面では、認可先がどちらの組織かは一目では分かりません。

誤った書き込みが起きなかった理由

戻しの関数には、書き込みの前に「接続が本当に相手の組織を向いているか」を確かめるガードを入れていました。当社が扱った組織では、同じ組織のレコードIDは先頭の数桁が共通でした。そこで、接続経由で相手組織のレイアウトIDを1件取り、その先頭8桁と、連携元キーに入っている相手レコードIDの先頭8桁を比べます。

次のコードは戻しの関数の一部を抜き出したものです(url_base と parentRecordId は、関数の前半で組織変数と連携元キーから組み立てています)。

/* 書き込み先の取り違えを防ぐ。接続が実際に指す組織と、
   書き込み先レコードの組織が一致するかを先頭8桁で確かめる */
expectedPrefix = parentRecordId.subString(0,8);
actualPrefix = "";
try
{
	layoutResponse = invokeurl
	[
		url :url_base + "settings/layouts?module=Quote_Items"
		type :GET
		connection:"parent_crm"
	];
	if(layoutResponse != null && layoutResponse.get("layouts") != null && layoutResponse.get("layouts").size() > 0)
	{
		actualPrefix = layoutResponse.get("layouts").get(0).get("id").toString().subString(0,8);
	}
}
catch (eProbe)
{
	info "接続先組織の確認でエラー: " + eProbe;
}
if(actualPrefix == "" || actualPrefix != expectedPrefix)
{
	info "★中止 接続先組織が想定と異なります。期待: " + expectedPrefix + " / 実際: " + actualPrefix;
	/* ここで担当者へ通知する(つまずき2) */
	return;
}

このガードが毎回中止していたため、子会社のレコードを誤って上書きすることはありませんでした。2つの組織をつなぐ関数では、「書き込む前に、書き込み先が正しいかを確かめる」処理を最初から入れておくことをおすすめします。先頭桁の照合を使う場合は、両方の組織で実際のIDを見て、桁が分かれることを確かめてから採用してください。

直し方

  1. 子会社側の接続を一度取り消し、再接続する。認可画面で親会社の組織を選ぶ
  2. 組織を選ぶ画面が出ない場合は、親会社だけに所属するユーザーで認可する
  3. 接続名とスコープは変えない(関数は接続名で参照しているため)
  4. 子会社側のレコードIDを引数に戻しの関数を手動で実行し、ログに「接続先組織を確認しました」と、相手側への反映が出ることを確かめる
  5. 止まっていたレコードを1件ずつ手動で再送する。接続を直しても自動では戻らない

今回は、止まっていた納期回答のほとんどをその日のうちに戻しました。残りの一部は、子会社側に入っていたのが回答ではなく入力漏れのエラー文だったため、戻すとそのエラー文が親会社側に表示されます。お客様へ先に連絡するまで保留にしました。再送の前に「戻す中身」を見ることも手順に入れておくと安全です。

もう1つ分かったのは、滞留の調べにくさです。戻しの関数は、送り元に「戻し済み」の印を書かない作りでした。そのため、どれが止まっているかは、毎回両方の組織を突き合わせないと分かりません。戻しの成否を送り元の項目に残す設計にしておけば、滞留は一覧で拾えます。

つまずき2:中止や失敗が、ログにしか出ていなかった

起きたこと

戻しの関数3本(納期回答・出荷実績・入庫実績)は、中止や失敗を info で関数のログに書くだけでした。関数のログは、誰かが開かない限り見えません。接続の誤りに約1週間気づけなかったのは、このためです。

対処:分岐ごとに通知を入れる

送信側の関数で使っていた失敗通知の共通関数を、戻しの3本にも入れました。中止や失敗の分岐ごとに呼び出し、追加した箇所は3本で16か所になりました。

設計で決めたことは次のとおりです。

決めたこと 理由
通知は自社側(送り元)のレコードに紐付ける 通知メールは自組織用の接続でレコードに紐付けて送る。相手組織のレコードIDを渡すと失敗する
通知の紐付け先は、共通関数が扱えるモジュールにする 共通関数のモジュール対応表にない履歴系のモジュールに付けると、メール内のリンクが壊れる。出荷・入庫は受注明細・発注明細に付けた
件名で原因の場所を分ける 組織間の戻しの失敗は「CRM処理エラー」とし、外部システムの失敗と見分けられるようにした
try の先頭で通知用の変数を初期化する 例外で catch に入ったときも、どのレコードの失敗かを通知できる
正常に「何もしない」ケースでは通知しない 回答がまだ来ていない、連携元キーが空(親会社由来ではない)は異常ではない。通知を出すと本当の異常が埋もれる

通知の本文には、「何が起きたか」「担当者が何をすべきか(レコードの値は変えない、など)」「直したあとにどう再実行するか」の3つを書きます。受け取った人が関数のログを開かなくても、次の行動が分かるようにするためです。

つまずき3:通知メールのリンクが、相手の組織を指していた

失敗通知のメールには、該当レコードを開くリンクを付けていました。ところが、リンクのURLが親会社のポータル名で固定されていたため、子会社から出た通知のリンクが親会社側の画面を開こうとしていました。

ポータル名を組織変数(関数から共通で読む設定値)に出し、組織ごとに値を入れる形に直しました。

portalName = ifnull(zoho.crm.getOrgVariable("crm_portal_name"),"").toString().trim();
if(portalName == "")
{
	/* 変数を登録する前に配備しても、従来どおりの動きになるようにする */
	portalName = "parent-portal";
}
recordUrl = "https://crm.zoho.jp/crm/" + portalName + "/tab/" + modulePath + "/" + recordId;

値が空のときは従来のポータル名に落ちるため、変数を登録する前に関数を配備しても挙動は変わりません。同じ関数を2つの組織に配備するときは、組織によって変わる値(ポータル名・相手先のID・送信モードなど)をコードに直書きしないことが原則です。

つまずき4:番号を振る前に送っていた

起きたこと

材質のマスタは、親会社で登録すると「MT-00001」のような材質コードを自動で振り、子会社と生産管理システムへ送る作りでした。ところが、送り方を切り替える分岐(子会社経由で送るかどうか)が、採番の処理より前に置かれていました。子会社経由のモードでは、採番をせずに送信して return していたため、コードが空のまま相手に届いていました。

対処:保存→採番→保存の確認→送信の順に固定する

関数の順番を次のように直しました(下のコードは、手順4・5にあたる部分の抜粋です。sendMode は組織変数から読んだ送り方の設定です)。

  1. レコードを読み直す
  2. コードが空なら次の番号を決め、自分の組織に保存する(このときは trigger を空にして、ワークフローを再起動させない)
  3. 保存の応答が成功でなければ、送信せずに止める
  4. コードがまだ空なら、送信せずに止める
  5. ここで初めて、送り方の分岐に入って相手へ送る
if(materialCode == "")
{
	info "【材質連携エラー】材質コードが空のため、連携を中止します。";
	return;
}
/* 採番と保存を終えてから、送り方を分岐する */
if(sendMode == "via_child")
{
	automation.sendMasterToChild("Material_Types",materialTypeId);
	return;
}

採番そのものにも注意点がありました。既存コードの最大値を文字列の並び順で取ると、英字の混じったコードや桁違いのコードが先頭に来て、番号が1に巻き戻ることがあります。数値として読める連番だけを見て最大値を取り、候補の番号が既に使われていないかを確かめてから保存しています。なお、COQL(SQLに似た検索)で上から一定件数だけ読んで最大値を探す作りにすると、読み込む件数を超えて英字のコードが並んだときに数値の連番を取りこぼします。読む件数の上限と、上限を超えたときの扱いも決めておきます。

既に欠けていたデータの直し方

コードが空のまま送られた材質は、子会社側では既に番号が付いていました。そこで新しい番号を振り直さず、子会社側の番号を正として親会社側を補完しました。番号を振り直すと、子会社と生産管理システムに同じ材質が2つの番号で並ぶためです。

この作業で1つ失敗もしています。動作確認のために親会社側で材質を編集保存したところ、事前の番号照合が足りず、子会社側の既存の番号が一時的に別の番号へ上書きされました。変更履歴で元の番号を確かめて戻し、再連携しています。試験の前に、両方の組織で対象レコードの番号を照合しておくことを手順に加えました。

確認は、送り元の関数の「成功」表示では済ませていません。受け手側の保存値と、受け手の関数ログにある外部システムの応答(HTTPステータスと応答本文)まで見て判定しました。

2つの組織をつなぐとき、何を確かめるか:設計チェックリスト

二つの組織を結ぶ連携で、送信前に宛先の組織を照合し、番号を振ってから送り、失敗を通知し、本番で一件通して確かめるという設計上の守りを順に示した図

設計の段階で決めておくこと:

  • 相手の組織へ書き込む関数に、書き込み前の「接続先の組織」照合を入れたか
  • 中止・失敗のすべての分岐に、担当者への通知を入れたか(catch を含む)
  • 通知は自社側のレコードに紐付け、件名で原因の場所が分かるか
  • 送り元に「戻し済み」「送信済み」の結果を残し、滞留を一覧で拾えるか
  • 組織ごとに変わる値(ポータル名・相手先ID・送信モード)を組織変数に出したか
  • 番号や必須値を作る処理を、送信の分岐より前に置いたか。空なら送らないか

本番切替の日に確かめること:

  • 相手組織用の接続が、本当に相手の本番組織を向いているかを実測したか(「接続が有効」だけで済ませない)
  • 送信と戻しを、本番でそれぞれ1件ずつ最後まで通したか
  • 両方の組織の組織変数を読み戻し、検証用の値が残っていないか確かめたか

まとめ

2つのZoho CRMをつなぐ連携では、つなぐ処理そのものよりも、「どちらを向いているか」「どの順で送るか」「失敗に誰が気づくか」で事故が起きます。今回、接続が誤った組織を向いていても実害が出なかったのは、書き込み前の照合ガードがあったからです。一方で、通知がなかったために1週間気づけませんでした。

ガードで止め、通知で知らせ、本番で1件通して確かめる。この3つを最初の設計と切替手順に入れておくと、グループ会社間の連携は落ち着いて運用できます。

コードを書かずにアプリ間をつなぐ選択肢としては、「Zoho Flowで業務効率を最大化:アプリ連携でワークフローを自動化」も参考にしてください。ワークフローから呼んだ関数が「成功」なのに動かないときの確かめ方は「Zoho CRMのワークフローが「成功」なのに動かない」で解説しています。

複数組織のCRMをつなぐ設計のご相談は、販売管理システムとCRMの統合構築サービスでお受けしています。

よくある質問

Zoho CRMの接続(Connections)とは何ですか?

Delugeの関数から、Zohoの別サービスや別の組織、外部のサービスのAPIを呼ぶための認証の設定です。どのサービスに・どの権限(スコープ)で・誰の認可でアクセスするかを保存し、関数では invokeurl に接続名を渡して使います。当社の案件では、両方の組織に所属するユーザーが認可した接続が、相手ではなく自分の組織を向いていました(原因は推定)。向き先は実際に呼び出して確かめる必要があります。

接続を作り直すと、関数のコードも直す必要がありますか?

接続名とスコープを変えずに、取り消して再認可すれば、関数のコードは直さずに済みます。関数は接続を名前で参照しているためです。再認可のときに相手の組織を選び直し、選べない場合は相手の組織だけに所属するユーザーで認可します。

接続を直せば、止まっていたデータは自動で相手に届きますか?

届きません。止まっていたのはワークフローから関数が起動した時点の処理なので、接続を直したあとに、対象のレコードを引数にして関数を手動で実行し直す必要があります。再送の前に、相手側で二重に計上されないかを確かめてください。

失敗通知を相手の組織のレコードに付けることはできますか?

当社の構成ではできませんでした。通知メールは自組織向けの接続でレコードに紐付けて送るため、相手の組織のレコードIDを渡すと失敗します。通知は自社側のレコード(送り元の明細など)に紐付け、本文に相手側の番号を書く形にしました。

組織ごとのレコードIDの先頭桁を使った照合は、Zohoの公式な仕様ですか?

公式に仕様として案内されたものではありません。当社が扱った組織では、同じ組織のレコードIDは先頭の数桁が共通で、組織が違えば異なっていたため、それを照合に使いました。採用する場合は、両方の組織で実際のIDを見て確かめてから使ってください。