進級処理の自動化とは、年度の変わり目に必要な「学生の学年を一つ上げる」「新入生を1年生にする」「新しい学年の履修レコードを作る」といった作業を、Zoho CRMの関数とワークフローで決まった時刻に実行することです。

この記事では、当社が支援している専門学校で設計・検証した進級処理をもとに、スケジュール関数の作り方と、本番前に確かめておくべき点を解説します。学生情報はZoho CRMの「連絡先」モジュールで管理し、在籍状況(ステータス)の項目で学年を表している前提です。

コードは、項目名を一般的な名前に置き換えています。

進級処理はどこまで自動化できるか:判定は人、書き換えは関数

年度切替を自動化するとき、最初に決めるのは「どこまでを人が行い、どこからを関数に任せるか」です。今回は次のように分けました。

時期 誰が やること
合格・入学手続きの時期 人(ボタン操作) 見込み客を学生情報(連絡先)へ転換する。在籍状況は「入学手続き前」で止める
3月末まで 人 進級判定に応じて、在籍状況を「進級」「仮進級」「〇年生(留年)」に変える
3月 人+関数 卒業要件を満たした学生を「卒業可能」→「卒業」へ移す
4月1日 0時過ぎ スケジュール関数 「入学手続き前」を1年生に、「進級」「仮進級」を次の学年に一括で更新する
4月1日以降 ワークフロー 在籍状況が2年生・3年生に変わったら、その学年の履修レコードを作る

ポイントは、進級できるかどうかの判断と、学年を書き換える作業を分けたことです。判断には仮進級や留年のような人の判断が入ります。一方、書き換えは件数が多く、4月1日という決まった日に一斉に行う必要があります。判断を3月末までに在籍状況として残しておけば、関数は「進級」「仮進級」と書かれた学生だけを機械的に処理すればよくなります。

新入生はいつ1年生にするか:「入学手続き前」で止める理由

新入生は、見込み客から学生情報へ転換した時点で1年生にしたくなります。今回はあえて、転換時の在籍状況を「入学手続き前」で止めました。

理由は、履修レコードの年度です。1年次の履修レコードは、科目マスターから学年に合う科目を引いて作ります。年度が切り替わる前に作ると、前の年度の情報で履修が作られるおそれがあります。

そこで、転換時は「入学手続き前」にするだけで履修は作らず、4月1日のスケジュール関数が1年生に上げるタイミングで、1年次の履修を作る関数を呼ぶ形にしました。こうしておけば、年度が変わる前のいつ転換しても、履修は新しい年度で作られます。

検証では、転換ボタンの関数と、転換後に入学許可書を送る関数の両方を読み、どちらも在籍状況を「入学手続き前」にするだけで履修を作らないことを確認しました。

スケジュール関数の処理の流れ

年度初日に動く関数が、在籍状況で学生を抜き出し、履歴から直前の学年を取り、新しい学年を決めて一括更新するまでの流れを示した図。

4月1日に動くスケジュール関数の処理は、次の順です。

  1. 組織変数から、APIのバージョン・データセンター・サンドボックスかどうかを読み、APIのURLを組み立てる
  2. COQLで在籍状況が「入学手続き前」の学生を抜き出し、1年生に更新して、1年次の履修を作る関数を呼ぶ
  3. COQLで在籍状況が「進級」「仮進級」の学生を、200件ずつページを送って全員分抜き出す
  4. 学生ごとに、ステータス履歴の関連リストから直前の学年(「〇年生」を含む値)を取る
  5. 直前の学年と「進級」「仮進級」の組み合わせから、新しい在籍状況を決める
  6. 更新内容を100件ずつまとめて、APIで一括更新する

新しい在籍状況の対応は次のとおりです。

直前の学年 3月末の在籍状況 4月1日の更新後
1年生 進級 2年生
1年生 仮進級 2年生(仮進級中)
2年生 進級 3年生
2年生 仮進級 3年生(仮進級中)

在籍状況を「進級」に変えた時点で、元の学年の情報は項目から消えます。そこで、在籍状況の変更履歴を残す関連リスト(ステータス履歴)から直前の学年を拾うのが、この設計の要です。

コード例(進級・仮進級の部分)

このコードは、ステータス履歴の関連リストが新しい順で返ることを前提にしています。履歴を先頭から見て、最初に見つかった「〇年生」を直前の学年として採るためです。古い順で返る環境では、1年生のころの履歴を拾って誤った学年に更新してしまいます。並び順は、本番前に必ず実際の環境で確かめてください(後述の「留年処理で確かめておく3つのこと」の3も同じ確認です)。

また、COQLは1回のリクエストで返す件数に上限があり、LIMIT を書かないと既定で200件までしか返りません(Zoho CRMの公式ドキュメントによる)。COQLで件数の多いデータを扱うときの別の落とし穴は「Zoho CRMウィジェットで取引先を名寄せ取込する|COQLの件数制限と空応答(204)の落とし穴」でもまとめています。対象が200件を超えても取りこぼさないよう、LIMIT 開始位置, 件数 の書き方で200件ずつページを送り、info の more_records が false になるまで取得します。

void schedule.AnnualPromotion()
{
	// 前提:ステータス履歴の関連リストは「新しい順」で返る(本番前に必ず確認する)
	// 1. 組織変数から API のURLを組み立てる(本番とサンドボックスで同じコードを使う)
	api_version = zoho.crm.getOrgVariable("version");
	dc = zoho.crm.getOrgVariable("dc");
	sandbox_val = zoho.crm.getOrgVariable("sandbox");
	if(api_version == null || api_version.toString().isEmpty())
	{
		api_version = "v8";
	}
	if(dc == null || dc.toString().isEmpty())
	{
		dc = "jp";
	}
	baseDomain = "www.zohoapis." + dc;
	if(sandbox_val != null && sandbox_val.toString().toLowerCase() == "true")
	{
		baseDomain = "sandbox.zohoapis." + dc;
	}
	api_url_base = "https://" + baseDomain + "/crm/" + api_version + "/";
	// 2. 「入学手続き前」→1年生の処理は、この例では省略しています(コードの後の説明を参照)
	// 3. 「進級」「仮進級」の学生を、200件ずつページを送って全員分集める
	//    先に全員分を集めてから更新する。取得しながら更新すると、更新済みの学生が条件から外れて
	//    開始位置がずれ、取りこぼしが出るため
	targets = List();
	pageList = {0,1,2,3,4,5,6,7,8,9};
	// 最大10ページ(2,000件)まで。対象がそれ以上になる場合は要素を増やす
	hasMore = false;
	for each  page in pageList
	{
		coqlMap = Map();
		coqlMap.put("select_query","select id, Status from Contacts where ((Status = '進級') or (Status = '仮進級')) order by id asc limit " + (page * 200) + ", 200");
		response = invokeurl
		[
			url :api_url_base + "coql"
			type :POST
			parameters:coqlMap.toString()
			connection:"crm_connection"
		];
		if(response.get("data") == null)
		{
			hasMore = false;
			break;
		}
		targets.addAll(response.get("data"));
		hasMore = false;
		if(response.get("info") != null && response.get("info").get("more_records") == true)
		{
			hasMore = true;
		}
		if(!hasMore)
		{
			break;
		}
	}
	if(hasMore)
	{
		info "【警告】対象が2,000件を超えています。pageList の要素を増やしてください。";
	}
	if(targets.isEmpty())
	{
		info "進級・仮進級の対象者はいません。";
		return;
	}
	updateList = List();
	for each  student in targets
	{
		studentId = student.get("id");
		currentStatus = student.get("Status");
		// 4. ステータス履歴(関連リスト)から直前の学年を取る(新しい順が前提)
		historyResponse = invokeurl
		[
			url :api_url_base + "Contacts/" + studentId + "/Status_History?fields=Status"
			type :GET
			connection:"crm_connection"
		];
		previousYear = "";
		if(historyResponse.get("data") != null)
		{
			for each  record in historyResponse.get("data")
			{
				recordStatus = record.get("Status");
				if(recordStatus != null && recordStatus.contains("年生"))
				{
					previousYear = recordStatus;
					break;
				}
			}
		}
		// 5. 新しい在籍状況を決める
		newStatus = "";
		if(previousYear.contains("1年生"))
		{
			if(currentStatus == "進級")
			{
				newStatus = "2年生";
			}
			else
			{
				newStatus = "2年生(仮進級中)";
			}
		}
		else if(previousYear.contains("2年生"))
		{
			if(currentStatus == "進級")
			{
				newStatus = "3年生";
			}
			else
			{
				newStatus = "3年生(仮進級中)";
			}
		}
		else
		{
			info "【対象外】直前の学年が取れません。学生ID: " + studentId + " / 履歴: " + previousYear;
		}
		if(newStatus != "")
		{
			updateList.add({"id":studentId,"Status":newStatus});
		}
		// 6. 100件ごとにまとめて更新する
		if(updateList.size() == 100)
		{
			updateResponse = invokeurl
			[
				url :api_url_base + "Contacts"
				type :PUT
				parameters:{"data":updateList}.toString()
				connection:"crm_connection"
			];
			info "更新結果(100件): " + updateResponse;
			updateList.clear();
		}
	}
	// 残りを更新する
	if(updateList.size() > 0)
	{
		updateResponse = invokeurl
		[
			url :api_url_base + "Contacts"
			type :PUT
			parameters:{"data":updateList}.toString()
			connection:"crm_connection"
		];
		info "更新結果(残り): " + updateResponse;
	}
}

Status_History は、ステータス履歴の関連リストのAPI名を置いた例です。実際のAPI名は環境ごとに違い、「relatedlist」に番号が付いたような、自動で付いた名前になっていることもあります。必ず実際の環境で確かめてください。

コード中の2(「入学手続き前」を1年生にする部分)は、紙面の都合で省いています。実際には同じ関数の前半に置き、3と同じようにCOQLで「入学手続き前」の学生を200件ずつ集めてから1年生に更新し、続けて1年次の履修を作る関数をAPI経由で呼びます。このとき関数の呼び出しにAPIキーが要るため、組織変数にAPIキーが入っていない場合は、履修作成を飛ばしてログに警告を残す作りにしています。

検証で分かったこと:2・3年次の履修は範囲外だった

コードを読んで検証すると、この進級関数は在籍状況を書き換えるだけで、2年次・3年次の履修レコードは作らないことが分かりました。1年次の履修だけが、「入学手続き前」から1年生にするときに作られます。

そこで、2・3年次の履修は、進級関数の中に足すのではなく、別の関数をワークフローから動かす形で補いました。

  • ワークフロールールの条件:在籍状況が「2年生」「2年生(仮進級中)」「3年生」「3年生(仮進級中)」のいずれかに変更されたとき
  • 実行する関数:学年に合う科目を科目マスターから取り、履修レコードを100件ずつまとめて作る
  • 重複の防止:同じ学生・同じ学年・同じ年度の履修が1件でもあれば、作らずに終了する

こうしておくと、4月1日の一括更新でも、4月以降に人が在籍状況を手で変えた場合でも、同じ関数で履修が作られます。進級関数は「学年の書き換え」だけに集中できます。

サンドボックスでは、在籍状況をAPIで変えて、次の4つを確かめました(2026年2月)。

  1. 1年生から2年生に変えると、2年次の科目分の履修が作られる
  2. 2年生 → 1年生 → 2年生と戻しても、履修は増えない(重複しない)
  3. 2年生から3年生に変えると、3年次の科目分だけが追加される
  4. 3年生から2年生に戻しても、2年次の履修は既にあるため増えない

4つとも想定どおりで、APIで在籍状況を更新したときにもワークフローが動くことを確認できました。

なお、この記事の時点で確認できているのは、この履修作成のワークフロー側(2026年2月、サンドボックス)です。進級関数そのものをサンドボックスで実行した結果は、まだ確認できていません。進級関数については、後述のチェックリスト10の手順で試す前提で書いています。

日付を基準にしたワークフローで年度切替を組もうとする場合は、基準日が過去になったレコードの扱いに注意が要ります。詳しくは「Zoho CRMの日付ベースのワークフローが発火しない?|基準日が過去のレコードの扱いと代わりの設計」をご覧ください。

ワークフローの条件には、「次のいずれかに変更されたとき」という単純な比較だけを使うのがおすすめです。条件に数式や関数を入れると、「Function specified in criteria has returned null value」というエラーが出る場合がありました。また、関数アクションには引数として必ずレコードのIDを渡してください。渡し忘れると、関数の中でIDが空になります。

留年した学生の履修はどう扱うか:確かめておく3つのこと

留年が決まった学生は、前の学年の履修レコードを「再履修」扱いに一括で更新します。この関数を検証したとき、次の3点を本番前の確認事項として挙げました。

  1. COQLのルックアップ条件の書き方。 連絡先の参照項目で絞るときは Contacts.id = の形で書くのが確実です。Contacts = '…' でも通る環境はありますが、書き方をそろえておくと迷いません
  2. 更新したい項目が数式項目でないか。 「成績評価」のような項目が数式で作られていると、値を直接書き込めません。その場合は、数式の元になっている選択リストなどの項目を更新する形に変えます
  3. ステータス履歴の並び順。 履歴が「新しい順」か「古い順」かで、拾う学年が変わります。留年のときに更新したいのは直前の学年なので、並び順を必ず確かめます

進級関数の「直前の学年を取る」処理も、3つ目の並び順に依存しています。進級と留年の両方で同じ確認が必要です。

年度切替前の事前確認チェックリスト

本番の4月1日を迎える前に、次の順で確かめます。

  1. 組織変数(APIのバージョン、データセンター、サンドボックスかどうか、APIキー)が、本番とサンドボックスのそれぞれで正しく入っている
  2. ステータス履歴の関連リストのAPI名を、実際の環境で確かめた
  3. ステータス履歴の並び順(新しい順か古い順か)を確かめた
  4. 在籍状況の選択肢(入学手続き前、1〜3年生、進級、仮進級、〇年生(仮進級中)、留年、卒業可能、卒業)が、コードの文字列と一字一句同じである
  5. 科目マスターに、1〜3年次の科目が登録されている
  6. 1年次の履修を作る関数と、2・3年次の履修を作る関数・ワークフローが、本番に登録されている
  7. スケジュール関数の実行日時が、4月1日の0時過ぎに設定されている
  8. 新入生は全員「入学手続き前」まで転換が済んでいる
  9. 進級・仮進級・留年が決まった学生は、3月末までに在籍状況を変えてある(「1年生」のままの学生は進級関数の対象にならない)
  10. サンドボックスで、次の3パターンを試した
    • 入学手続き前 → 1年生(1年次の履修が作られる)
    • 進級 → 次の学年
    • 仮進級 → 次の学年(仮進級中)
  11. 留年処理で更新する項目が、数式項目でない

9は、関数ではなく運用の確認です。進級関数は「進級」「仮進級」と書かれた学生だけを動かすため、在籍状況を変え忘れた学生は4月1日以降もそのまま残ります。その場合は、人が在籍状況を「2年生」などに変えれば、ワークフローで履修が作られます。

自動化しなかったこと

すべてを自動にしたわけではありません。次の2つは、人の作業として手順書に残しました。

  • 編入生の対応。 2年次に編入する学生は、1年次の科目の一部と2年次の科目の両方を履修するため、自動だけでは履修が揃いません。人数が少ないため、当面は手動で補う運用にしました
  • 進級の判定そのもの。 仮進級や留年には人の判断が入るため、在籍状況の変更は人が行います

年に1回しか動かない処理は、動かなかったときに気づくのが翌年になりがちです。だからこそ、自動にする範囲を絞り、人の作業は手順書に残し、本番前にサンドボックスで確かめる、という3点を組み合わせるのが安全です。

Delugeでの自動化の基本は、「はじめてのDeluge(デリュージ)」やDeluge関数でタスクの自動生成を設定した記事でも紹介しています。サンドボックスで確かめた関数を本番へ移すときの注意点は「Zoho CRMのサンドボックスから本番へ反映する落とし穴」にまとめています。学校や研修機関での年度切替の設計についてご相談があれば、Zoho CRMカスタマイズ・設定代行サービスからお気軽にお問い合わせください。

よくある質問

進級処理の判定までZoho CRMで自動にできますか?

単位数や出席率などの条件が項目で表せれば、判定の候補を出すことはできます。ただし仮進級や留年のように人の判断が入るものは、在籍状況を人が変える運用にしておくほうが、誤って学年を進めてしまう事故を防げます。

スケジュール関数で数百件を一度に更新しても大丈夫ですか?

気をつける点は取得と更新の2か所です。取得に使うCOQLは、LIMITを書かないと1回200件までしか返らないため、200件ずつページを送って全員分を集めます。更新は100件ずつまとめてAPIで送る作りにしています。関数の実行時間やAPIの利用量が気になる場合は、Zohoの公式ドキュメントで最新の上限を確認し、必要なら処理を分割してください。

APIで在籍状況を更新したとき、ワークフロールールは動きますか?

今回のサンドボックス検証(2026年2月)では、APIで在籍状況を2年生・3年生に更新したときに、在籍状況の変更をきっかけとするワークフローが動き、履修レコードが作られました。環境によって挙動が違う可能性があるため、本番前にサンドボックスで確かめることをおすすめします。

留年した学生の履修はどう扱えばよいですか?

前の学年の履修レコードを「再履修」扱いに一括で更新する関数で対応できます。ただし、更新したい項目が数式項目だと値を直接書き込めないため、項目の種類を先に確認してください。