Skip to content

日次スケジュールサマリ通知 ​

日程調整の抜け漏れを拾い直すため、業務側チャンネルへ毎朝サマリを流す仕組み。

通知内容 ​

  1. 提案から3営業日以上動きのない日程調整 — 企業へ提案したまま確定していない調整
  2. 本日の確定予定 — その日に予定されている面接など

メンションは付けない。毎朝必ず流れる定期便で名指しすると通知疲れを起こし、 かえって読まれなくなるため。

該当0件でも送る。無音だと「バッチが動いていないのか、本当に0件なのか」が 区別できず、結局誰も信用しなくなる。

見え方 ​

Block Kit で組み、滞留と本日の確定予定を別々の attachment に分けている。 色は attachment 単位でしか付けられないため、1枚にまとめると滞留の重さを色で 表せなくなるからである。

:memo: [BUSON] 日次スケジュールサマリ 2026/08/26(水)|滞留 2件・本日の確定 2件

┃ 警告 提案から3営業日以上動きのない日程調整(2件)        ← 赤バー
┃ 赤 山田 太郎 / 株式会社サンプル / 一次面接
┃  提案 08/17(月)(6営業日経過) ・ 候補2枠(候補日はすべて経過・要再提案)
┃ 黄 田中 次郎 / 株式会社テスト / 二次面接
┃  提案 08/21(金)(3営業日経過) ・ 候補1枠(最短 09/01(火) 11:00)

┃ 予定 本日の確定予定(2件)                              ← 青バー
┃ `10:00〜11:00` 佐藤 花子 / 株式会社サンプル / 最終面接
┃ `14:00〜15:00` 鈴木 一郎 / 株式会社テスト / 一次面接
  • 見出し行(プッシュ通知にもそのまま出る)に件数まで載せる。attachment の中身は プッシュ通知・チャンネル一覧のプレビューに出ないため、この1行だけで 「今日は対応が必要か」を判断できるようにしている
  • 求職者名は管理画面の求職者詳細へのリンクになる(APP_BASE_URL 未設定なら素の文字列)
  • 「最短 …」はまだ到来していない候補日のうち最も早いもの。候補日がすべて過ぎた 調整は「候補日はすべて経過・要再提案」と出す(過去日付を「最短」と出すと 予約できる枠が残っているように読めるため)
  • 該当0件の日はヘッダー1行(✅ / 📅)だけに畳む

色と信号 ​

状態attachment の色行頭の絵文字
滞留なし緑 #2eb886—
3〜4営業日の滞留黄 #e2b203:large_yellow_circle:
5営業日以上の滞留が1件でもある赤 #d92d20:red_circle:
本日の確定予定(常に情報色)青 #2f6fed—

attachment の色は「その日で最も重い滞留」で決まる。行頭の信号を併記しているのは、 色が出ない通知プレビューでも重さが読めるようにするため。

文字数上限への対処 ​

Slack の section は 3000 字が上限で、超えるとメッセージが丸ごと 400 で弾かれる。 それは「通知が消える」ことを意味する(このバッチが防ごうとしている失敗そのもの)ため、 一覧は文字数予算で複数セクションに分割する。黙って切り詰めることはしない。

なぜ「空き」ではなく「提案」を見るのか ​

提案 は空き行とは別の新規行として作られ、空き行はDB上で分割も消費もされない (フロントの splitAvailableSchedulesByOccupied が表示上だけ重なりを差し引いている)。

そのため「空きのまま提案されていない」を行単位で検知すると、次のケースで永久に 鳴り続ける。

  • 求職者が5時間の空きを入れ、そのうち1時間だけを提案した
  • 3日分の空きのうち2日分だけを提案した

業務側が実際に動いた記録である 提案 行を起点にすることで、この誤検知を構造的に 避けている。

なお、「空きが登録されたが一度も提案されていない」はこのサマリでは拾えない。 その入口は /mypage の空き時間登録リアルタイム通知が担う (E2E手順 の「5.」を参照)。

滞留判定の詳細 ​

  • 単位: 選考(application_id) × ステージ(stage_id)。1回の調整で候補日程を複数 提案するのが通常運用なので、行単位で出すと同じ調整が何行にも増殖する
  • 営業日: 土日を除外して数える。祝日マスタが存在しないため祝日は考慮しない (3営業日の閾値が最大1〜2日ずれる。アラート用途では許容範囲と判断)
  • 起点: created_at(提案した時点)と updated_at(最後に動いた時点)の両方が 閾値を超えていること。updated_at も見るのは、確定→提案への差し戻し (リスケの正規手順)直後の行が古い created_at のせいで即座に鳴るのを防ぐため
  • DB側の絞り込み: 暦日ベースのプレフィルタで母集合を絞り、営業日の厳密な判定は アプリ側で行う。N営業日の経過は必ずN暦日以上の経過を伴うので取りこぼしは出ない
  • 表示上限: 15件。超過分は …ほかN件 として件数を明示する(黙って打ち切らない)

実行方法 ​

Cloud Scheduler から認証付きで叩く。

POST /api/v1/notifications/daily-schedule-summary
Authorization: Bearer ${API_SERVICE_KEY}

スケジュール: 平日の朝9時(0 9 * * 1-5 / タイムゾーン Asia/Tokyo)。

Cloud Scheduler ジョブは CI が作る ​

api-v* タグを打つと、deploy-api.yml の sync-scheduler ジョブが daily-schedule-summary を 作成または更新 する。手作業でのジョブ作成は不要。

  • スケジュールを変えたい: deploy-api.yml の SCHEDULE を書き換えて api-v* タグを打つ。コンソールを触らないので変更が履歴に残る
  • 宛先URL: Cloud Run の実URL(*.run.app)を gcloud run services describe で 取得して使う。Cloudflare Access 配下のドメインに投げると Access に弾かれるため
  • API のデプロイとは別ジョブにしてある。権限不足などでスケジューラ同期が失敗しても API のデプロイ自体は完了済みになる(ワークフローは赤くなるので気づける)
  • リトライは上限付き(3回 / 最大15分)。無制限だと、実際には成功していた実行が タイムアウト扱いになったときSlackへ何通も出てしまう

一度だけ手動が必要

デプロイ用サービスアカウント(GCP_SERVICE_ACCOUNT)に roles/cloudscheduler.admin を付与しておくこと。この付与だけは CI から実行できない (自分自身に権限を付けることはできないため)。Cloud Scheduler API の有効化も同様。

bash
gcloud services enable cloudscheduler.googleapis.com --project buson-482306

gcloud projects add-iam-policy-binding buson-482306 \
  --member "serviceAccount:<GCP_SERVICE_ACCOUNT>" \
  --role roles/cloudscheduler.admin

既存の enrich / crawler 用スケジューラ(docs/operations/cloud-run-setup.md)は 従来どおり手動作成のまま。CI 管理はこのジョブのみ。

レスポンス ​

json
{
  "data": {
    "staleProposalCount": 1,
    "todayConfirmedCount": 2,
    "notified": true,
    "skipped": false
  }
}
  • notified: true = Slack送信成功
  • skipped: true = Webhook URL未設定などで送信未実行

内容だけ確認する(送信しない) ​

?dry_run=true を付けると、Slackへ送らずに内容を返す。素のテキスト版が message、 実際に送るペイロードが payload。

bash
# 内容をひと目で確認する
curl -s -X POST \
  "http://127.0.0.1:8787/api/v1/notifications/daily-schedule-summary?dry_run=true" \
  -H "Authorization: Bearer ${API_SERVICE_KEY}" | jq -r '.data.message'

# 見た目を確認する(出力を Block Kit Builder に貼る)
curl -s -X POST \
  "http://127.0.0.1:8787/api/v1/notifications/daily-schedule-summary?dry_run=true" \
  -H "Authorization: Bearer ${API_SERVICE_KEY}" | jq '.data.payload'

Block Kit Builder: https://app.slack.com/block-kit-builder

ローカル開発(NODE_ENV != production)では認証がダミーユーザーで通るため、 Authorization ヘッダなしでも実行できる。

関連環境変数 ​

変数名説明
SLACK_SCHEDULE_WEBHOOK_URL送信先(未設定なら SLACK_WEBHOOK_URL にフォールバック)
APP_BASE_URL求職者詳細リンクの組み立てに使う(未設定ならリンク行を省略)
API_SERVICE_KEYCloud Scheduler からのサービス間通信認証(authMiddleware が検証)

SLACK_SCHEDULE_MENTION_USER_IDS はこのサマリでは使わない(メンションしないため)。