【GAS × LLM】自動でシフト表からGoogleカレンダーの予定を作る
先日、知り合いから 「バイトの出勤予定をカレンダーに表示しているんだけど、手作業で予定を入れていくのが面倒すぎる」 と相談を受けました。
そんなときこそ自動化です。
スプレッドシート上にあるバイトのシフト表を、Google Apps Script(GAS)を使って、共有のGoogleカレンダーに反映する仕組みを作成しました。
できたもの
スプレッドシートのシフト表のセルの中に時刻もしくはイベントタイトルなどを記入します。
すると、カレンダー上に予定が自動作成されます。
この共有カレンダーを関係者に共有しておくことで、スマホなどから簡単に予定を確認できます。
使うもの
- Googleスプレッドシート
- シフト表はここに書きます
- Googleカレンダー
- 予定の作成先です
- Google Apps Script(GAS)
- シフト表が更新された際に動作するコードはここに書きます。
- OpenAIのAPIキー
- 曖昧な記載内容をうまく判定させるために、LLMに解釈させます。
Google Apps Script(GAS)とは
Googleが提供するJavaScript系のプログラミングプラットフォームです。
GmailやGoogleドライブ, スプレッドシートなどのサービスを自動化するために使われることが多いです。
Googleアカウントがあれば無料で使えます。ブラウザ上で動作するため、デプロイしたい場合もサーバーの用意なども必要ありません。
手順
以下の手順で進めます。
- OpenAIのAPIキーを取得
- 反映先のGoogleカレンダーを作成する
- Googleスプレッドシート上でシフト表を準備する
- Googleスプレッドシート上でGASを新規作成する
- 環境変数を登録する
- コードを書く
- トリガーを設定する
- 実際に試す
OpenAIのAPIキーを取得
一つ目の下準備です。
以下のページからAPIキーを作成してください。
初めて作成する方は以下の記事を参照してください。
反映先のGoogleカレンダーを作成する
二つ目の下準備です。
+ボタンを押して、新しいカレンダーを作成します。
任意の名前で新規作成してください。
今回はシンプルに「シフト表」としました。
作成したカレンダーを下にスクロールして「カレンダーID」を取得しておきます。
Googleスプレッドシート上でシフト表を準備する
今回は実際のシフト表の形式をもとに内容をダミーデータに書き換えました。
コピペ用
月日 曜日 週 足立 飯野 上田 遠藤 緒方 ヘルプ 午前 ランチタイム ティータイム ディナータイム ピッチ 備考
10:00-12:00 12:00-15:00 15:00-17:30 18:00-22:30 19:00-19:30
5/1 木 1 09:00-22:30 9:30-16:30 10:00-24:00 18:30-20:00 工藤:12:30-14:30 サウナ小次郎さん:500件行き尽くして分かった最強のサウナbest3
5/2 金 1 09:00-18:00 午後NG 9:30-19:00 10:00-24:00 NG G社 樋口さん「月5,000円から始める!積立投資セミナー」 TV取材予定
5/5 土 1 NG 13:00-17:00 18:30-20:00 セミナー:「LLM時代にどう生きるか」 セミナー:「LLM時代にどう生きるか」
5/6 日 2 09:00-22:30 9:30-19:00 NG 野口さん_キーボード沼バー TV取材予定
5/5 月 2 09:00-18:00 NG NG 10:00-24:00 hoge協会 10周年記念イベント hoge協会 10周年記念イベント hoge協会 10周年記念イベント hoge協会 10周年記念イベント
5/6 火 2 09:00-22:30 NG NG お休みにしたいが要相談 18:30-20:00 "工藤:12:30-14:30
毛塚:09:00-21:00" ヘルプあり
5/7 水 2 09:00-18:00 9:30-16:30 14:00-19:00 10:00-24:00 NG 視察があるかも?
5/8 木 2 09:00-22:30 9:30-16:30 13:00-17:00 10:00-24:00 18:30-20:00
5/9 金 2 09:00-18:00 9:30-16:30 14:00-19:00 お休みにしたいが要相談 NG M社 福沢さん(調整中)
コピペするだけだと上手くセルごとに分割されないので、以下の記事を参照して適切に分割してください。
以降の手順ではこのシートに対応した内容でコードを作成しています。
すでにご自身で作成している人はそれを使っても問題ありませんが、コードは適切に書き換えてください。
- 固定のメンバーがいる(水色)
- ヘルプのメンバーは同じセルに複数人入ることがある(青色)
- 会場にはイベントの時間帯のスロットがある(オレンジ色)
- 備考欄には全体で共有すべきメモ書きが入る(赤色)
※ ヘルプの列で、同じセルに複数人の予定が書かれていることの若干もどかしさを感じますが、人間の見やすさの考慮や、実際のシフト表が既にそのような運用がされていることから、今回はこのままいきます。
現状の運用に実装を合わせるのではなく、効率的に実装しやすいように運用を変えるのが適切かと思いますが、なかなかうまくはいかないものです。
Googleスプレッドシート上でGASを新規作成する
メニューの「拡張機能」→「Apps Script」からGASを新規作成しましょう。
すると自動でそのスプレッドシートと紐付いた状態のGASファイルが作成されます。
今回は触れませんが、GAS単体で使うこともできます。
環境変数を登録する
先に環境変数を登録しておきます。
「プロジェクトの設定」(歯車のアイコン)→「スクリプト プロパティ」→「スクリプト プロパティを追加」から以下の二つを登録します。
- OPENAI_API_KEY
- CALENDAR_ID
コードを書く
さて、ついにメインディッシュです。
まずは、コードの全文を掲載します。これをコードエディタに貼り付けてください。
長いため折りたたんでいます。
コード全文
/************* 設定項目 *************/
// 環境変数
//「プロジェクトの設定」(歯車のアイコン)→「スクリプト プロパティ」→「スクリプト プロパティを追加」
const OPENAI_API_KEY = PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');
const CALENDAR_ID = PropertiesService.getScriptProperties().getProperty('CALENDAR_ID');
// トリガーするシートの名前
const SHEET_NAME = "シフト表"
/*******************************************************************/
/* OpenAI 呼び出し */
/*******************************************************************/
function callOpenAI(prompt) {
const url = 'https://api.openai.com/v1/chat/completions';
const payload = {
model: 'gpt-4.1',
messages: [{ role: 'user', content: prompt }],
temperature: 0,
};
const resp = UrlFetchApp.fetch(url, {
method: 'post',
contentType: 'application/json',
headers: { Authorization: 'Bearer ' + OPENAI_API_KEY },
payload: JSON.stringify(payload),
muteHttpExceptions: true,
});
const result = JSON.parse(resp.getContentText());
return result.choices[0].message.content;
}
/*******************************************************************/
/* Google カレンダーへイベント登録 */
/*******************************************************************/
function createCalendarEvents(eventsJson) {
const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
const tz = Session.getScriptTimeZone();
const dayStart = new Date(eventsJson.date + 'T00:00:00');
const dayEnd = new Date(eventsJson.date + 'T23:59:59');
/* 1) 同日の既存イベントを削除 */
calendar.getEvents(dayStart, dayEnd).forEach(ev => ev.deleteEvent());
/* 2) 各イベントを登録 */
eventsJson.events.forEach(evt => {
// startとendが両方nullの場合、終日イベントとして扱う
if (evt.start && evt.end) {
// 時刻が指定されている通常イベント
const start = new Date(evt.start);
const end = new Date(evt.end);
calendar.createEvent(evt.title, start, end);
} else {
// 終日イベントの場合、日付のみで作成
calendar.createAllDayEvent(evt.title, new Date(eventsJson.date));
}
});
}
/*******************************************************************/
/* プロンプト生成(スプレッドシート 1 行分) */
/*******************************************************************/
function generatePrompt(rowValues, headers) {
const date = Utilities.formatDate(new Date(rowValues[0]), 'JST', 'yyyy-MM-dd');
let shiftData = `## シフト表データ(${date}):\n`;
headers.forEach((header, idx) => {
if (header === '月日' || header === '曜日' || header === '週') return;
const cell = rowValues[idx];
shiftData += `- ${header}:${cell ? cell : '記載なし'}\n`;
});
return `
あなたは **シフト表の曖昧な情報を解析し、Google カレンダー用の JSON を生成するアシスタント** です。
以下の **シフト表データ** を読み取り、指示どおりに整形された JSON を出力してください。
**コードフェンスを付けず、純粋な JSON テキストのみ** を返してください。
${shiftData}
---
## 変換ルール
1. **否定キーワード/空欄**
- 「NG」「難」「1日難」「終日NG」「午後NG」「午前NG」や空欄は **出勤なし** とみなす。つまり、予定を作成しない。
2. **調整中を示すキーワード**
- 「調整中」「仮」「休みにしたい」「候補」などが含まれる場合 **title** フィールドには冒頭に「【仮】」をつける。(例:「【仮】文字列全体」)
3. **時刻が記載されている場合**
- 明確な時刻が書かれている場合を常に **最優先** する。
例)「ピッチ 18:00-21:00」とあれば 18:00-21:00 を採用。
3. **時刻の記載がない場合**
① 以下のキーワードが含まれる場合はマッピングを適用する。
| キーワード | 開始 | 終了 |
|-----------------|------|------|
| 午前 | 10:00| 12:00|
| ランチタイム | 12:00| 15:00|
| ティータイム | 15:00| 17:30|
| ディナータイム | 18:00| 22:30|
| ピッチ | 19:00| 19:30|
② 「備考」はstart/end ともに null とする。
③ 上記①のいずれにも該当せず、時刻が不明な場合はstart/end ともに null とする。
4. **複数の「時間帯+イベント名」が 1 セルにある場合**
- スペースまたは改行で区切り、**各組み合わせを別々のイベント** として出力。
- セル内に書かれたイベント名を **title** フィールドに採用し、列見出し(header)は無視してよい。
5. **イベント名の取扱い**
- セルの中には主催団体名やタイトル, メモ情報など多様な文字列が入る。いかなる場合でもその文字列全体を入れる
(例: 「hoge協議会」,「fugaイベント」, 「福沢さん←日時調整中」)は、 **title** フィールドに “イベント種類\_その文字列全体” を入れる。(例:「午前\_hoge協会」,「ディナータイム\_fugaイベント, ピッチ\_福沢さん←日時調整中」」)
---
## JSON 出力フォーマット
{
"date": "YYYY-MM-DD",
"events": [
{
"title": "イベント名",
"start": "YYYY-MM-DDTHH:MM:SS",
"end": "YYYY-MM-DDTHH:MM:SS"
}
…
]
}
`;
};
/*******************************************************************/
/* シート編集トリガー(メイン関数) */
/*******************************************************************/
function onSheetEdit(e) {
const sheet = e.source.getActiveSheet();
// 対象シートかどうか判定
if (!sheet.getName().includes(SHEET_NAME)) {
Logger.log(`対象外シート「${sheet.getName()}」→ 処理終了`);
return;
}
const range = e.range;
const startRow = range.getRow();
const numRows = range.getNumRows();
const startCol = range.getColumn();
const numCols = range.getNumColumns();
Logger.log(`変更範囲: 行 ${startRow}〜${startRow + numRows - 1}, 列 ${startCol}〜${startCol + numCols - 1}`);
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const changedValues = sheet.getRange(startRow, 1, numRows, sheet.getLastColumn()).getValues();
for (let i = 0; i < numRows; i++) {
const rowIndex = startRow + i;
const rowValues = changedValues[i];
/* ─────────── スキップ条件 ─────────── */
// ① ヘッダ行
if (rowIndex === 1 || rowIndex === 2) {
Logger.log(`ヘッダ行(行 ${rowIndex})→ 処理スキップ`);
continue;
}
// ② A〜C 列のみ変更
if (startCol <= 3 && (startCol + numCols - 1) <= 3) {
Logger.log(`変更が A〜C 列のみ(行 ${rowIndex})→ 処理スキップ`);
continue;
}
// ③ 月日セルが空欄
if (rowValues[0] === '' || rowValues[0] === null) {
Logger.log(`月日が空欄(行 ${rowIndex})→ 処理スキップ`);
continue;
}
/* ──────────────────────────────────── */
const prompt = generatePrompt(rowValues, headers);
Logger.log(`プロンプト(行 ${rowIndex}):\n${prompt}`);
const response = callOpenAI(prompt);
Logger.log(`LLMレスポンス(行 ${rowIndex}):\n${response}`);
// コードフェンス除去
let jsonText = response.trim()
.replace(/^\s*```(?:json)?\s*/, '')
.replace(/\s*```$/, '');
try {
const eventsJson = JSON.parse(jsonText);
createCalendarEvents(eventsJson);
} catch (err) {
Logger.log(`JSON 解析エラー(行 ${rowIndex}): ${err}`);
}
}
}
/*******************************************************************/
/* トリガー停止用ダミー */
/*******************************************************************/
function triggerStop(e) {
Logger.log('トリガー、一時停止中');
}
大まかな流れとしては、以下の3段階になります
- スプレッドシートの変更を検知する(onSheetEdit)
- OpenAIのAPIに送信してJSON形式に予定情報を整形する(callOpenAI)
- 予定情報をもとにGoogleカレンダーに予定を作成する(createCalendarEvents)
例えば、あるセルの中に出勤時刻を記載すると、その行(=その日付)のデータをまとめて読み取って、予定を作成します。
スプレッドシートの変更を検知する(onSheetEdit)
後ほどトリガーを設定することで、何らかのセルの値が変更された際にonSheetEditを実行できるようにします。
無駄にOpenAI APIへのコールが起こらないように2つのスキップ処理が入っています。
- 変更の検知対象は全てのシートのため、別シートに特定の名前が含まれているシート以外はスキップするようにしています
- 別シートにメモ書きを作成しただけでも発火するのを防ぐ
- 行列の見出しに対しての変更
- 日付を伸ばした際に全行に発火するのを防ぐ
function onSheetEdit(e) {
const sheet = e.source.getActiveSheet();
// 対象シートかどうか判定
if (!sheet.getName().includes(SHEET_NAME)) {
Logger.log(`対象外シート「${sheet.getName()}」→ 処理終了`);
return;
}
(中略)
/* ─────────── スキップ条件 ─────────── */
// ① ヘッダ行
if (rowIndex === 1 || rowIndex === 2) {
Logger.log(`ヘッダ行(行 ${rowIndex})→ 処理スキップ`);
continue;
}
// ② A〜C 列のみ変更
if (startCol <= 3 && (startCol + numCols - 1) <= 3) {
Logger.log(`変更が A〜C 列のみ(行 ${rowIndex})→ 処理スキップ`);
continue;
}
// ③ 月日セルが空欄
if (rowValues[0] === '' || rowValues[0] === null) {
Logger.log(`月日が空欄(行 ${rowIndex})→ 処理スキップ`);
continue;
}
/* ──────────────────────────────────── */
プロンプトに今回の行情報を埋め込み、OpenAIのAPIをコールして、最後に予定を作成しています。
const prompt = generatePrompt(rowValues, headers);
Logger.log(`プロンプト(行 ${rowIndex}):\n${prompt}`);
const response = callOpenAI(prompt);
Logger.log(`LLMレスポンス(行 ${rowIndex}):\n${response}`);
// コードフェンス除去
let jsonText = response.trim()
.replace(/^\s*```(?:json)?\s*/, '')
.replace(/\s*```$/, '');
try {
const eventsJson = JSON.parse(jsonText);
createCalendarEvents(eventsJson);
} catch (err) {
Logger.log(`JSON 解析エラー(行 ${rowIndex}): ${err}`);
}
## シフト表データ(2025-05-06):
- 足立: 09:00-22:30
- 飯野:NG
- 上田:NG
- 遠藤:お休みにしたいが要相談
- 緒方:18:30-20:00
- ヘルプ:工藤:12:30-14:30
毛塚:09:00-21:00
- 午前:記載なし
- ランチタイム:記載なし
- ティータイム:記載なし
- ディナータイム:記載なし
- ピッチ:記載なし
- 備考:記載なし
- :実行
{
"date": "2025-05-06",
"events": [
{
"title": "足立",
"start": "2025-05-06T09:00:00",
"end": "2025-05-06T22:30:00"
},
{
"title": "【仮】遠藤_お休みにしたいが要相談",
"start": null,
"end": null
},
{
"title": "ヘルプ_工藤",
"start": "2025-05-06T12:30:00",
"end": "2025-05-06T14:30:00"
},
{
"title": "ヘルプ_毛塚",
"start": "2025-05-06T09:00:00",
"end": "2025-05-06T21:00:00"
},
{
"title": "緒方",
"start": "2025-05-06T18:30:00",
"end": "2025-05-06T20:00:00"
}
]
}
OpenAIのAPIに送信してJSON形式に予定情報を整形する(callOpenAI)
シフト表のデータから、JSON形式に変更してもらいます。
実際にコードで書くと場合分けが非常に面倒な部分ですが、自然言語で指示するだけで処理してくれます。
プロンプト自体もLLMと相談しながら、実際に動作を試しつつ作成しました。
プロンプト生成
/*******************************************************************/
/* プロンプト生成(スプレッドシート 1 行分) */
/*******************************************************************/
function generatePrompt(rowValues, headers) {
const date = Utilities.formatDate(new Date(rowValues[0]), 'JST', 'yyyy-MM-dd');
let shiftData = `## シフト表データ(${date}):\n`;
headers.forEach((header, idx) => {
if (header === '月日' || header === '曜日' || header === '週') return;
const cell = rowValues[idx];
shiftData += `- ${header}:${cell ? cell : '記載なし'}\n`;
});
return `
あなたは **シフト表の曖昧な情報を解析し、Google カレンダー用の JSON を生成するアシスタント** です。
以下の **シフト表データ** を読み取り、指示どおりに整形された JSON を出力してください。
**コードフェンスを付けず、純粋な JSON テキストのみ** を返してください。
${shiftData}
---
## 変換ルール
1. **否定キーワード/空欄**
- 「NG」「難」「1日難」「終日NG」「午後NG」「午前NG」や空欄は **出勤なし** とみなす。つまり、予定を作成しない。
2. **調整中を示すキーワード**
- 「調整中」「仮」「休みにしたい」「候補」などが含まれる場合 **title** フィールドには冒頭に「【仮】」をつける。(例:「【仮】文字列全体」)
3. **時刻が記載されている場合**
- 明確な時刻が書かれている場合を常に **最優先** する。
例)「ピッチ 18:00-21:00」とあれば 18:00-21:00 を採用。
3. **時刻の記載がない場合**
① 以下のキーワードが含まれる場合はマッピングを適用する。
| キーワード | 開始 | 終了 |
|-----------------|------|------|
| 午前 | 10:00| 12:00|
| ランチタイム | 12:00| 15:00|
| ティータイム | 15:00| 17:30|
| ディナータイム | 18:00| 22:30|
| ピッチ | 19:00| 19:30|
② 「備考」はstart/end ともに null とする。
③ 上記①のいずれにも該当せず、時刻が不明な場合はstart/end ともに null とする。
4. **複数の「時間帯+イベント名」が 1 セルにある場合**
- スペースまたは改行で区切り、**各組み合わせを別々のイベント** として出力。
- セル内に書かれたイベント名を **title** フィールドに採用し、列見出し(header)は無視してよい。
5. **イベント名の取扱い**
- セルの中には主催団体名やタイトル, メモ情報など多様な文字列が入る。いかなる場合でもその文字列全体を入れる
(例: 「hoge協議会」,「fugaイベント」, 「福沢さん←日時調整中」)は、 **title** フィールドに “イベント種類\_その文字列全体” を入れる。(例:「午前\_hoge協会」,「ディナータイム\_fugaイベント, ピッチ\_福沢さん←日時調整中」」)
---
## JSON 出力フォーマット
{
"date": "YYYY-MM-DD",
"events": [
{
"title": "イベント名",
"start": "YYYY-MM-DDTHH:MM:SS",
"end": "YYYY-MM-DDTHH:MM:SS"
}
…
]
}
`;
};
実行部分
const prompt = generatePrompt(rowValues, headers);
Logger.log(`プロンプト(行 ${rowIndex}):\n${prompt}`);
const response = callOpenAI(prompt);
Logger.log(`LLMレスポンス(行 ${rowIndex}):\n${response}`);
予定情報をもとにGoogleカレンダーに予定を作成する(createCalendarEvents)
挙動としては、該当日の全ての予定を削除した後、新規作成します。差分を取った方が効率的かもしれませんが、今回は簡単のためにこのような形にしました。
開始時刻、終了時刻がある場合は時刻ありの予定、それ以外は終日の予定として作成します。
GASの便利なところとして、各Googleサービスに特化した組み込みのクラスが標準で用意されています。これらのクラスを通じて、対応するGoogleサービス内のデータや機能にアクセスできます。
/*******************************************************************/
/* Google カレンダーへイベント登録 */
/*******************************************************************/
function createCalendarEvents(eventsJson) {
const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
const tz = Session.getScriptTimeZone();
const dayStart = new Date(eventsJson.date + 'T00:00:00');
const dayEnd = new Date(eventsJson.date + 'T23:59:59');
/* 1) 同日の既存イベントを削除 */
calendar.getEvents(dayStart, dayEnd).forEach(ev => ev.deleteEvent());
/* 2) 各イベントを登録 */
eventsJson.events.forEach(evt => {
// startとendが両方nullの場合、終日イベントとして扱う
if (evt.start && evt.end) {
// 時刻が指定されている通常イベント
const start = new Date(evt.start);
const end = new Date(evt.end);
calendar.createEvent(evt.title, start, end);
} else {
// 終日イベントの場合、日付のみで作成
calendar.createAllDayEvent(evt.title, new Date(eventsJson.date));
}
});
}
トリガーを設定する
右下の「+ トリガーを追加」からトリガーを追加します。
画像の通り設定してください。
「イベントの種類を選択」について注意点が必要です。
編集時(onEdit)はセルの内容の変更のみに反応します。
変更時(onChange)はセルの内容の変更に加えて、シートの追加や行列の追加などにも反応します。
Geminiの説明
ただし、変更時(onChange)が上位互換な訳ではなく、受け取るイベントオブジェクトeの中身にも情報に差があります。
今回はコードの中で、まとめて複数のセルを変更した際にその変更範囲を受け取る箇所があり、それができるのは編集時(onEdit)のため、そちらを選びます。
/*******************************************************************/
/* シート編集トリガー(メイン関数) */
/*******************************************************************/
function onSheetEdit(e) {
const sheet = e.source.getActiveSheet();
(中略)
const range = e.range; // ←編集時(onEdit)でしかrangeは使えない
const startRow = range.getRow();
const numRows = range.getNumRows();
const startCol = range.getColumn();
const numCols = range.getNumColumns();
なお、変数名をonSheetEdit
ではなく特別な関数名であるonEdit
とすると、「シンプルなトリガー」という形でトリガー設定無しで自動で発火するようになるのですが、その場合はセキュリティ上の理由から実行できる内容に制限がかかります。
具体的にはUrlFetchApp(外部サービスへのリクエスト)や GmailApp(メール送信)など、外部サービスにアクセスする操作は、シンプルなトリガーでは許可されていません。
そのため今回はあえてonSheetEdit
という形で明示的にトリガーを設定します。
2025/05/27 10:59:56 エラー Exception: 指定された権限では UrlFetchApp.fetch を呼び出すことができません。必要な権限: https://www.googleapis.com/auth/script.external_request
at callOpenAI(コード:20:28)
at onEdit(コード:184:22)
実際に試す
適当なセルに何か記載してみてください。
今回は5/1(木)の備考欄に記入してみます。
すると5〜10秒ほどで予定が作成されました。
反映されるまで何度か画面をリロードしてください。
ログの画面でも正常に動作していることがわかります。
もし認証画面や認証エラーが出た際は認証を行なってください。
補足:GASの認証について
初回の動作や新たなGoogleサービスにアクセスしようとする場合は認証画面が出ます。
こちらの記事を参照してください。
補足:トリガー停止用ダミーについて
スプレッドシート側でまとまった変更をする場合は、先にトリガーの関数をダミーに変更しておくと無駄に発火せずに済みます。
/*******************************************************************/
/* トリガー停止用ダミー */
/*******************************************************************/
function triggerStop(e) {
Logger.log('トリガー、一時停止中');
}
最後に
この記事では実際のシフト表を題材にカレンダーへの予定の反映の自動化を行いました。
実際には記入方法のブレやエラー対応などの処理が甘い部分があるとは思いますが、まずはMVPとして使ってみようと思います。
クラウド上ですべて完結すること、外部APIやLLMへのリクエストが行えること、データベースとしてのスプレッドシートを使えることなど、使い方次第でよりたくさんの活用方法が考えられそうです。
参考
以下の記事では商用AIエージェントの初期開発をGASやDifyで実施したとの内容を紹介されています。UIは本質ではないという学びのある記事でしたのでぜひ読んでみてください。
Lambdaなどを使わずに実装できるのは便利そう。
Discussion