kintoneとGoogleカレンダーの連携。予定を二重に入れない作り方

公開 書いた人 大林 諒

kintoneで訪問や面談の予定を管理し始めると、ほぼ確実に「Googleカレンダーにも出してほしい」という声が出ます。連携を組むと、次に起きるのは同じ予定がカレンダーに2つ並ぶ、kintoneでキャンセルしたのにカレンダーに残る、という事故です。この記事では、kintoneを予定の正、Googleカレンダーを表示先と決めたうえで、レコードから予定を作り、時間の変更とキャンセルまで追従させる仕組みをApps Scriptで組みます。あわせて、kintoneの中で同じ担当者に同じ時間の予定を入れさせない設定も作ります。アプリを作って公開したことがあり、スタンダードコース以上を契約している人を前提にしています。

この記事の内容
  1. 先に決めること
  2. 設計表
  3. 作る手順
  4. 更新・削除を同期させるためのIDの持ち方
  5. kintoneの中で同じ時間に予定を入れさせない
  6. 予約・訪問・面談の管理での使い方
  7. 双方向にしないほうがいい理由と例外
  8. できないことと、その代わり
  9. よく詰まるところ
  10. 次にやること

先に決めること

kintoneのカレンダー形式で足りるか

外に出す前に、kintoneの中で済むかを先に見ます。一覧の表示形式には「カレンダー形式」があり、日付の欄にレコードを並べて表示できます。

  • カレンダーの日付にできるのは「日付」「日時」「作成日時」「更新日時」のフィールド
  • タイトルに出せるのは1つのフィールドだけ(文字列(1行)、ユーザー選択、ルックアップ、計算など)
  • 1ページに500件まで表示される
  • 今日の日付が強調表示される
  • 日付の右上の「+」を押すと、その日付が入った状態で新しいレコードの画面が開く
  • モバイル版でもカレンダー形式の一覧は表示できる

サイボウズのヘルプにも書かれているとおり、kintoneにはGaroonのようなスケジュール機能はありません。カレンダー形式は、日付か日時のフィールドの値でレコードを日付の欄に並べて見せる表示です。

判断の目安は次の表です。

状況向く形
予定を見る人が全員kintoneを毎日開くカレンダー形式の一覧で足りる
担当者が外出先でスマホのGoogleカレンダーを見て動いているGoogleカレンダーに出す
個人の予定と訪問予定を1つの画面で見比べたいGoogleカレンダーに出す
Garoonかサイボウズ Officeを使っている公式のスケジュール連携プラグインを先に検討する

実際に回っている会社では、両方を使っていることが多いです。事務の人はkintoneのカレンダー形式で全体を見て、外を回る担当者はGoogleカレンダーで自分の予定を見ます。

どちらを正にするか

連携を組む前に、予定の正しい値はどちらにあるかを決めます。この記事ではkintoneを正にします。予定を入れる、時間を変える、キャンセルする、はすべてkintoneで行い、Googleカレンダーは見るだけの場所にします。顧客や案件とつながっているのはkintoneの側なので、正をkintoneに置くと、訪問の記録や商談の履歴と予定がずれません。

出し方を3つから選ぶ

Googleカレンダーに出す方法は3つあります。どれもAPIかWebhookかプラグインを使うので、ライトコースではできません。料金表ではライトコースが月額1,000円/1ユーザー、スタンダードコースが月額1,800円/1ユーザー(どちらも税抜、最低10ユーザー)で、外部サービス連携やプラグインなどの拡張機能はスタンダードコースからです。

方法費用更新・キャンセルの追従向く会社
連携サービス・プラグイン製品ごとの月額製品による作る人がいない。予算はある
Apps Script+REST API無料(Googleアカウント)自分で作る。この記事の方法コードを貼って動かせる人が1人いる
連携ツール(Zapier、Make、n8nなど)ツールの料金組み方次第すでに連携ツールを使っている

連携サービスを選ぶときは、kintoneで時間を変えたときとキャンセルしたときにカレンダーがどうなるかを、試用の段階で必ず試します。新しい予定を作るところまでしか面倒を見ない製品もあります。

連携ツールは、kintoneの何をきっかけにするかで差が出ます。Zapierのkintone連携のページでは、kintoneのきっかけ(トリガー)は「New Record」の1つだけです。新規の予定は出せても、変更やキャンセルを拾うには、kintoneのWebhookで「レコードの編集」を連携ツールに知らせる組み方が要ります。

どのカレンダーに出すか

担当者それぞれの個人カレンダーに書き込むより、用途ごとに専用のカレンダーを1つ作り、そこへ出すほうが崩れません。

  • 訪問予定、採用面談、会議室Aのように、用途か設備ごとに1つ作る
  • 作るのは会社の管理用Googleアカウントにする(担当者個人のアカウントだと、退職でまるごと止まる)
  • 担当者には「予定の詳細の表示」で共有する

共有の権限を見るだけにしておくのが要点です。担当者がGoogleカレンダーで予定をドラッグして動かせると、kintoneと時間がずれます。見るだけにしておけば、変えたい人はkintoneを開くしかなくなり、正が1つに保てます。

【画面:Googleカレンダーの「設定と共有」で、担当者を「予定の詳細の表示」で追加した状態】

設計表

予定のアプリを1つ作ります。顧客管理のアプリがあれば、顧客名はそこからルックアップで取ります。

フィールド名種類設定
顧客名ルックアップ顧客管理の顧客名をコピー元にする
担当者ユーザー選択必須。リマインダーの通知先にも使う
開始日時日時必須
終了日時日時必須
場所文字列(1行)訪問先の住所。カレンダーの場所に入る
状態ドロップダウン予定/完了/キャンセル。初期値は予定
内容メモ文字列(複数行)目的や持ち物。カレンダーの説明に入る
表示名文字列(1行)自動計算。カレンダー形式のタイトルに使う
予定ID文字列(1行)スクリプトが書き込む。人は触らない

フィールドコードはフィールド名と同じ文字にそろえます。スクリプトの中でフィールドコードを使うので、あとで変えるとスクリプトも直すことになります。

表示名には次の式を入れます。日付に日時のフィールドを選んだカレンダー形式は、タイトルの前に開始時刻を自動で付けます。タイトルに出せるのは1つのフィールドだけなので、終了時刻と顧客名をここで1つにまとめておくと、枠の中で「10:00 〜11:00 株式会社サンプル」と読めます。開始時刻まで式に入れると「10:00 10:00」と二重になります。

"〜" & DATE_FORMAT(終了日時, "HH:mm", "Asia/Tokyo") & " " & 顧客名

日時フィールドは見る人のタイムゾーンで表示されますが、計算式の中では第3引数で指定したタイムゾーンで文字にします。日本時間で出したいので "Asia/Tokyo" にします。日付だけのフィールドや時刻のフィールドをこの関数に渡すときは、ここを "Etc/GMT" にしないと9時間ずれます。

予定IDが、カレンダーの予定とレコードを1対1に結ぶ唯一の手がかりです。ここが空なら「まだカレンダーに無い」、入っていれば「この予定を書き換える」とスクリプトが判断します。

作る手順

アプリを作ってカレンダー形式の一覧を足す

  1. 設計表のフィールドを配置し、アプリを公開します。
  2. レコードの一覧画面右上の歯車アイコン「アプリを設定」をクリックします。
  3. 「一覧」タブを開きます。
  4. 「一覧を追加する」アイコンをクリックします。
  5. 「一覧名」を「訪問カレンダー」にします。
  6. 「レコード一覧の表示形式」で「カレンダー形式」を選びます。
  7. 「日付」に開始日時を選びます。
  8. 「タイトル」に表示名を選びます。
  9. 「絞り込み」で状態が「キャンセル」以外のレコードに絞ります。
  10. 「保存」をクリックします。
  11. 作った一覧を一番上にドラッグして、デフォルトの一覧にします。
  12. 「アプリを更新」をクリックします。

担当者ごとに見たいなら、絞り込みに担当者を足した一覧を人数分作ります。一覧は1アプリにたくさん作れるので、担当者が10人程度なら人数分並べても困りません。

一覧の設定で「カレンダー形式」を選び、日付に開始日時、タイトルに表示名を指定した状態
一覧の設定で「カレンダー形式」を選び、日付に開始日時、タイトルに表示名を指定した状態
訪問カレンダーの一覧。日付の枠に「10:00 〜11:00 株式会社サンプル」のように並んだ状態
訪問カレンダーの一覧。日付の枠に「10:00 〜11:00 株式会社サンプル」のように並んだ状態

削除を止め、予定IDを触らせない

この仕組みは、kintoneで変わったレコードを定期的に読みに行く作りです。削除されたレコードは読みに行っても見つからないので、カレンダーの予定が残ります。そこで、予定は削除せず「キャンセル」にする運用にし、削除の権限を外します。

  1. 「設定」タブで「アクセス権」の「アプリ」を開きます。
  2. Everyoneの行の「レコード削除」のチェックを外します。
  3. アプリの管理者の行を足し、「レコード削除」を含めてすべてにチェックを入れます。
  4. 管理者の行の左端にある「ドラッグして並び替え」アイコンをドラッグし、Everyoneの行より上に移します。
  5. 「保存」をクリックします。

1人のユーザーに複数の権限があるときは、上の行の設定が優先されます。足した行は下に入るので、並び替えないとEveryoneの行が勝ち、管理者も削除できません。

続けて、予定IDを人が書き換えられないようにします。

  1. 「設定」タブで「アクセス権」の「フィールド」を開きます。
  2. 「追加する」をクリックし、フィールドに予定IDを選びます。
  3. Everyoneに閲覧だけを許可し、編集のチェックを外します。
  4. 「保存」をクリックし、「アプリを更新」をクリックします。

APIトークンに与えた権限は、アプリ・レコード・フィールドのアクセス権の設定より優先されます。人は予定IDを編集できなくても、スクリプトはAPIトークンで書き込めます。

フィールドのアクセス権。予定IDでEveryoneが閲覧だけになった状態
フィールドのアクセス権。予定IDでEveryoneが閲覧だけになった状態

APIトークンを作る

  1. 「設定」タブの「カスタマイズ/サービス連携」にある「APIトークン」を開きます。
  2. 「生成する」をクリックします。
  3. 許可する操作は、レコードの閲覧と編集の2つだけにチェックを入れます。
  4. 「メモ」に「Googleカレンダー連携用」と書きます。
  5. 「保存」をクリックします。
  6. 「アプリを更新」をクリックします。

トークンは1アプリ20個まで作れます。用途ごとに分けておくと、連携を止めたいときにそのトークンだけ消せます。トークンは第三者に知られないように扱います。スクリプトの本文には書かず、次の手順のスクリプト プロパティに入れます。

Googleカレンダーを作り、IDを控える

  1. 管理用のGoogleアカウントでGoogleカレンダーを開きます。
  2. 左側の「他のカレンダー」の横の追加アイコンから「新しいカレンダーを作成」を選びます。
  3. 名前を「訪問予定」にして「カレンダーを作成」を選びます。
  4. 作ったカレンダーのその他アイコンから「設定と共有」を開きます。
  5. 左側の「カレンダーの統合」をクリックし、カレンダーIDをコピーします。
  6. 「共有する相手」で担当者を追加し、権限を「予定の詳細の表示」にします。

Apps Scriptを置く

  1. 管理用のGoogleアカウントでApps Scriptのエディタを開き、新しいプロジェクトを作ります。
  2. 左側の「プロジェクト設定」をクリックします。
  3. 「スクリプト プロパティを追加」をクリックします。
  4. 次の4つを「プロパティ」と「値」に入れます。
  5. 「スクリプト プロパティを保存」をクリックします。
プロパティ値の例
KINTONE_DOMAINexample.cybozu.com
KINTONE_APP_ID123
KINTONE_TOKEN作ったAPIトークン
CALENDAR_IDxxxxxxxx@group.calendar.google.com
  1. エディタに次のコードを貼って保存します。
const P = PropertiesService.getScriptProperties();
const DOMAIN = P.getProperty('KINTONE_DOMAIN');
const APP = P.getProperty('KINTONE_APP_ID');
const TOKEN = P.getProperty('KINTONE_TOKEN');

function syncKintoneToCalendar() {
  const lock = LockService.getScriptLock();
  if (!lock.tryLock(10000)) return; // 前の実行が終わっていなければ何もしない
  try {
    const cal = CalendarApp.getCalendarById(P.getProperty('CALENDAR_ID'));
    if (!cal) throw new Error('カレンダーが見つからないか、権限がない');
    const since = P.getProperty('LAST_SYNC') || '2026-01-01T00:00:00Z';
    const startedAt = toKintoneTime(new Date(Date.now() - 5 * 60 * 1000));
    const records = fetchUpdated(since);
    records.forEach(function (r) { syncOne(cal, r); });
    // 500件ちょうどなら続きがあるので、最後に処理した更新日時から再開する
    const next = records.length === 500
      ? records[records.length - 1]['更新日時'].value
      : startedAt;
    P.setProperty('LAST_SYNC', next);
  } finally {
    lock.releaseLock();
  }
}

function fetchUpdated(since) {
  const q = '更新日時 >= "' + since + '" order by 更新日時 asc limit 500';
  const url = 'https://' + DOMAIN + '/k/v1/records.json?app=' + APP +
    '&query=' + encodeURIComponent(q);
  const res = UrlFetchApp.fetch(url, {
    method: 'get',
    headers: { 'X-Cybozu-API-Token': TOKEN },
    muteHttpExceptions: true
  });
  if (res.getResponseCode() !== 200) throw new Error(res.getContentText());
  return JSON.parse(res.getContentText()).records;
}

function syncOne(cal, r) {
  const id = r['$id'].value;
  const savedId = r['予定ID'].value;
  const start = new Date(r['開始日時'].value);
  const end = new Date(r['終了日時'].value);
  if (!(end > start)) return; // 時間が壊れているレコードは飛ばす

  let ev = savedId ? cal.getEventById(savedId) : findByTag(cal, id, start, end);

  if (r['状態'].value === 'キャンセル') {
    if (ev) ev.deleteEvent();
    if (savedId) writeBack(id, '');
    return;
  }

  const names = r['担当者'].value.map(function (u) { return u.name; }).join('・');
  const title = '訪問 ' + r['顧客名'].value + '(' + names + ')';
  const desc = r['内容メモ'].value + '\n\nkintone: https://' + DOMAIN +
    '/k/' + APP + '/show#record=' + id;

  if (!ev) {
    ev = cal.createEvent(title, start, end, {
      description: desc,
      location: r['場所'].value
    });
    ev.setTag('kintoneRecordId', id); // 書き戻しに失敗したときの目印
  } else {
    ev.setTitle(title);
    ev.setTime(start, end);
    ev.setDescription(desc);
    ev.setLocation(r['場所'].value);
  }
  if (ev.getId() !== savedId) writeBack(id, ev.getId());
}

function findByTag(cal, id, start, end) {
  const hits = cal.getEvents(start, end).filter(function (e) {
    return e.getTag('kintoneRecordId') === id;
  });
  return hits.length ? hits[0] : null;
}

function writeBack(id, eventId) {
  const res = UrlFetchApp.fetch('https://' + DOMAIN + '/k/v1/record.json', {
    method: 'put',
    contentType: 'application/json',
    headers: { 'X-Cybozu-API-Token': TOKEN },
    payload: JSON.stringify({ app: APP, id: id, record: { '予定ID': { value: eventId } } }),
    muteHttpExceptions: true
  });
  if (res.getResponseCode() !== 200) throw new Error(res.getContentText());
}

function toKintoneTime(d) {
  return d.toISOString().replace(/\.\d{3}Z$/, 'Z');
}
  1. 関数に syncKintoneToCalendar を選んで一度だけ手で実行します。初回はGoogleアカウントへのアクセスの許可を求められるので許可します。
  2. kintoneでテスト用の予定を1件入れ、もう一度実行して、カレンダーに予定が1件だけ出たことと、レコードの予定IDが埋まったことを確かめます。
  3. 左側の「トリガー」をクリックします。
  4. 右下の「トリガーを追加」をクリックします。
  5. 実行する関数に syncKintoneToCalendar、イベントのソースに「時間主導型」を選び、間隔を5分おきにします。
  6. 「保存」をクリックします。

【画面:Apps Scriptの「プロジェクト設定」で、4つのスクリプト プロパティが並んだ状態】

【画面:トリガーの追加。syncKintoneToCalendarを5分おきに実行する設定】

【画面:Googleカレンダーの訪問予定に、kintoneから作られた予定が並んだ状態】

コードは、前回から更新されたレコードだけを取りに行きます。kintoneの日時はAPIでは協定世界時の「2026-11-05T01:00:00Z」の形で返ってくるので、そのまま new Date() に渡せば日本時間の10時として正しく扱われます。前回の時刻から5分戻して取りに行くのは、kintoneとGoogleの時計のずれで取りこぼさないためです。同じレコードを2回処理しても、予定IDがあれば書き換えになるだけなので、二重には作られません。

更新・削除を同期させるためのIDの持ち方

二重登録が起きるのは、ほぼすべて「カレンダーに予定を作ったのに、そのIDをkintoneに戻せなかった」ときです。次の実行で予定IDが空のままなので、スクリプトはもう一度予定を作ります。ここを3段で防いでいます。

  • 予定を作ったら、すぐにそのIDをレコードの予定IDに書き戻す
  • 予定を作るときに、カレンダーの予定の側にもレコードIDを目印(タグ)として付けておく
  • 予定IDが空のレコードは、作る前に同じ時間帯の予定から目印が一致するものを探し、見つかればそれを使う

書き戻すのは、Apps Scriptの予定のIDです。このIDはiCalUIDと呼ばれる形式で、繰り返しの予定では同じIDを複数の予定が持つことがあります。kintoneから作る予定は1回ずつの予定なので、この点は問題になりません。Googleカレンダーの側で繰り返しの予定に変えられると壊れるので、共有を見るだけにしておく理由がここにもあります。

キャンセルは削除で表しません。状態をキャンセルにすると、スクリプトがカレンダーの予定を消し、予定IDを空に戻します。レコードは残るので、いつ誰がキャンセルしたかもkintoneに残ります。

書き戻すとレコードの更新日時が変わるので、次の実行でそのレコードがもう一度読まれます。予定IDが入っているので書き換えだけ行い、IDも変わらないので書き戻しはしません。1回余計に読むだけで、繰り返しにはなりません。

同時に2回動くと、両方が予定IDの空を見て予定を2つ作ります。コードの最初で LockService を使っているのはこのためです。前の実行が終わっていなければ、後から来た実行は何もせずに終わります。

kintoneの中で同じ時間に予定を入れさせない

カレンダーへの二重登録とは別に、そもそも同じ担当者に同じ時間の予定を2つ入れさせない設定も要ります。

枠が決まっている予約なら標準機能で止められる

日時フィールドには「値の重複を禁止する」を設定できます。複数のレコードで同じ日時を入れられなくなります。会議室1つ、面談の枠が30分刻みで決まっている、のように「同じ開始時刻を2件入れられなければよい」場合はこれで足ります。

ただし、比べるのは開始日時の値が完全に同じかどうかだけです。10時から11時の予定に、10時半から始まる予定は入ってしまいます。担当者が複数いる場合も、別の担当者の同じ時刻まで止まります。

時間の重なりで止めるならJavaScript

担当者ごとに時間の重なりを止めるには、保存するときのイベントでチェックします。レコード追加画面と編集画面で保存ボタンを押したときに、同じ時間帯の予定を検索し、同じ担当者がいれば保存を止めます。

(function () {
  const events = [
    'app.record.create.submit', 'app.record.edit.submit',
    'mobile.app.record.create.submit', 'mobile.app.record.edit.submit'
  ];
  kintone.events.on(events, function (event) {
    const rec = event.record;
    const start = rec['開始日時'].value;
    const end = rec['終了日時'].value;
    if (rec['状態'].value === 'キャンセル') return event;
    const me = rec['担当者'].value.map(function (u) { return u.code; });
    const query = '開始日時 < "' + end + '" and 終了日時 > "' + start + '"' +
      ' and 状態 not in ("キャンセル") limit 500';
    return kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', {
      app: event.appId, query: query, fields: ['$id', '担当者', '顧客名']
    }).then(function (resp) {
      const hit = resp.records.filter(function (r) {
        if (event.recordId && r['$id'].value === String(event.recordId)) return false;
        return r['担当者'].value.some(function (u) { return me.indexOf(u.code) >= 0; });
      });
      if (hit.length) {
        event.error = '同じ時間に ' + hit[0]['顧客名'].value + ' の予定が入っています';
      }
      return event;
    });
  });
})();

event.error に文字を入れて返すと、画面の上部にエラーが出て保存が止まります。このチェックは、レコード追加画面と編集画面で保存ボタンを押したときに動く仕組みです。実際の運用では、ファイルの読み込みでまとめて入れた予定には重なりが残っていたことがあるので、取り込んだあとはカレンダー形式の一覧で重なりを目で確かめます。JavaScriptファイルは「設定」タブの「カスタマイズ/サービス連携」にある「JavaScript / CSSでカスタマイズ」から「アップロードして追加」で読み込みます。PC用とスマートフォン用は別々に指定するので、スマホから入れる予定も止めたいなら両方に同じファイルを入れます。

同じ担当者の同じ時間に予定を入れようとして、画面上部にエラーが出た状態
同じ担当者の同じ時間に予定を入れようとして、画面上部にエラーが出た状態

予約・訪問・面談の管理での使い方

訪問

この記事の形そのままです。カレンダーは「訪問予定」を1つ作り、担当者全員に見るだけで共有します。予定のタイトルに担当者名を入れているので、全員の予定が1つのカレンダーに並んでも誰の訪問か分かります。前日の夕方に担当者へ知らせたいなら、kintoneの「リマインダーの条件通知」で、基準日を開始日時、1日前、絶対時刻を17時、通知先を担当者のフィールドにします。リマインダーは1アプリ10件まで作れます。

面談

採用面談は、面接官ごとに面談を見たい一方で、応募者の情報を全員に見せたくない、という事情があります。カレンダーのタイトルには応募者名を出さず、「一次面談」のように種類だけを出し、詳細はkintoneのリンクから開く形にします。説明欄にレコードのURLを入れているのはこのためです。kintone側は、レコードのアクセス権で面接官と人事だけが見られるように絞ります。

応募者をGoogleカレンダーの招待で呼ぶことは、この仕組みではしません。createEvent の招待メールを送る設定は初期値で送らない側になっていて、コードでもそのままにしています。社外の人への案内は、日時を確定させたあとに人が送ります。

予約(会議室・社用車)

設備の予約は、設備ごとにカレンダーを1つ作ります。予約アプリに「設備」のドロップダウンを置き、スクリプトの中で設備の値によって出すカレンダーを変えます。設備ごとの重なりを止めるなら、JavaScriptの検索条件に設備を足し、担当者の比較を外します。

設備の空きを社員が見る場所は、Googleカレンダーでもkintoneの一覧でもかまいません。予約を入れる場所だけは、kintoneの1か所に決めます。

双方向にしないほうがいい理由と例外

Googleカレンダーで動かした予定をkintoneに戻したくなる場面はあります。それでも、最初は片方向で作ります。理由は4つです。

  • 両方で同時に変えたとき、どちらを残すかの決まりが要る。決まりを作っても、現場はどちらが勝ったか分からない
  • kintoneの保存で動く重なりのチェックを、Googleカレンダーでのドラッグは通らない。二重予約がそこから入る
  • Googleカレンダーの側では、繰り返しにする、ゲストを足す、別のカレンダーに移す、が自由にできる。kintoneのレコードと1対1が崩れる
  • 片方の書き込みがもう片方の更新として読まれ、書き込みが行ったり来たりする作りになりやすい

例外は2つあります。1つは、Googleカレンダーの予定を読むだけの使い方です。担当者の個人カレンダーの予定と重なっていないかを、Apps Scriptの getEvents で調べて、kintoneのレコードに「重なりあり」と書き込みます。予定そのものは書き換えないので、正は崩れません。

もう1つは、担当者がスマホのGoogleカレンダーでしか時間を変えられない現場です。この場合は戻す項目を開始日時と終了日時の2つに絞り、Googleカレンダーの予定の最終更新日時がkintoneの更新日時より新しいときだけ戻す、と決めて作ります。タイトルや場所まで戻し始めると、上の4つの問題がそのまま出ます。

できないことと、その代わり

やりたいこと標準でできるか代わりの手
kintoneの標準機能でGoogleカレンダーに予定を出すできないApps Script、連携ツール、連携サービス
ライトコースで連携するできない(APIトークン、Webhook、プラグインが使えない)スタンダードコースにする
Garoonのようなスケジュール機能を使うできない(公式に機能は無いとある)Googleカレンダーに出す。カレンダー表示を足すプラグイン。担当者ごとの一覧を作る
時間の重なりで保存を止めるできない(同じ値の重複だけ止められる)保存時のJavaScriptでチェックする
レコードの削除をカレンダーに反映する読みに行く方式では拾えない削除させずキャンセルにする。Webhookの「レコードの削除」を連携ツールで受ける
一瞬でカレンダーに反映するApps Scriptの定期実行は最短1分おきWebhookで連携ツールに知らせる

Webhookで削除を受ける場合は、送られてくる中身に注意します。削除の通知に入るのはレコード番号と削除した人と日時だけで、レコードの中身は入りません。予定IDも届かないので、カレンダー側で予定に付けた目印から探して消す作りにします。また、Excel・CSVの読み込みや、複数レコードをまとめて操作するAPIでは、Webhookは送られません。通知はドメイン単位で1分間に60回までで、それを超えた分は送られません。

よく詰まるところ

症状原因直し方
同じ予定がカレンダーに2つ出る予定IDの書き戻しに失敗した。または2つの実行が同時に動いた書き戻しを予定作成の直後に置く。予定に目印を付けて探す。LockServiceで同時実行を止める
kintoneで時間を変えてもカレンダーが古いままトリガーが止まっている。またはAPIトークンの権限が足りない「トリガー」の実行履歴を見る。APIトークンにレコードの閲覧と編集があるか確かめる
kintoneで消した予定がカレンダーに残る削除されたレコードは読みに行っても見つからない削除の権限を外し、キャンセルで表す。残った予定は手で消す
予定が9時間ずれる日付や時刻のフィールドを日時のつもりで扱った。またはDATE_FORMATのタイムゾーンの指定違い開始と終了は日時フィールドにする。日付・時刻のフィールドは “Etc/GMT” で文字にする
getCalendarById で null が返るカレンダーIDの貼り間違い。スクリプトを動かすアカウントがそのカレンダーにアクセスできない、または登録していない「カレンダーの統合」のIDを貼り直す。管理用アカウントで作ったカレンダーを使う。共有されたカレンダーなら管理用アカウントのカレンダー一覧に登録する
予定IDがあるのに「見つからない」になるCalendarApp.getEventById は既定のカレンダーしか探さない予定を探すときは取得したカレンダーの getEventById を使う
担当者が退職したら連携が止まったトリガーは作った人のアカウントで動く管理用アカウントでスクリプトとトリガーを作り直す
「Summary of failures for Apps Script」というメールが来るトリガーの実行が失敗しているApps Scriptの実行履歴でエラーの内容を見る。kintoneの応答なら本文にエラーが入っている
1分おきにしたら途中で止まる無料のGoogleアカウントはトリガーの実行時間が1日90分まで5分おきに戻す。Google Workspaceなら1日6時間まで
500件より多く更新したら一部が出ない1回の取得は500件までコードの続きから再開する処理を消していないか確かめる。最初の一括登録は分けて入れる
担当者がGoogleカレンダーで予定を動かしてしまった共有の権限が「予定の詳細を変更、表示可能」以上になっている「予定の詳細の表示」に下げる。動いた予定は次にkintoneが更新されたときに戻る
取り込んだ予定に重なりが残っていた保存時のJavaScriptは追加・編集画面で保存ボタンを押したときに動く仕組み取り込みのあとにカレンダー形式の一覧で重なりを確かめる
カレンダー形式で予定が抜けて見える1ページに500件まで状態や担当者で絞った一覧に分ける

kintone側のAPIの回数にも上限があります。スタンダードコースは1アプリあたり1日1万回です。5分おきの実行なら1日288回の取得と書き戻しの分で収まりますが、同じアプリをほかの連携からも読んでいるなら合計で数えます。Apps Scriptの側も、無料のGoogleアカウントは1日に作れる予定が5,000件、外部への通信が2万回までです。

次にやること

カレンダー形式の一覧の絞り込みや、担当者ごとの一覧の作り分けを詰めるなら「一覧の作り方。絞り込み・並び替え・カレンダー表示まで」が続きです。この記事のコードで使ったAPIトークンの扱いやレコードの取得・更新の書き方は「kintone REST APIの使い方。APIトークンの発行からレコードの取得・登録まで」で、1件ずつ確かめながら読めます。予定をGoogleスプレッドシートにも並べて集計したくなったら「kintoneとGoogleスプレッドシートの連携」が同じApps Scriptの組み方です。訪問予定を顧客や商談とつないで、訪問のたびに案件の進み具合を残したいなら「kintoneで案件管理を組む。顧客管理とつなぐ作り方」に進みます。

あわせて読む