education-cloud-course-catalog-migrate について
CSV、PDF、ウェブサイトのコースカタログを Education Cloud に取り込み、Learning と LearningCourse のレコードを作成する Agent スキルです。バッチ作成に加え、データの完全性やコース間の関係、既存レコードとの競合を確認します。
主な機能
- 01CSV・PDF・ウェブサイトからコース情報を抽出
- 02必須項目を検証し不足を特定
- 03講義と実験のコース関連を検出
- 04Learning と LearningCourse をバッチ作成
- 05競合を確認し解決方法を提示
活用シーン
- 01学部のコースカタログを Education Cloud に取り込み
- 02複数ソースのコース項目を整理
- 03取り込み前に既存コースと重複を確認
MCP 対応
Salesforce sObject REST API と Composite API でコースレコードを作成し、事前に API の通信経路を確認します。代替経路として他の Salesforce MCP やカスタム MCP の確認に触れています。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ forcedotcom/ sf-skills --skill education-cloud-course-catalog-migrateインストール後、Claude Code で/education-cloud-course-catalog-migrateを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/gotchas-detail.mdgotchas.mdlarge-catalog-handling.mdscripts/batch_courses.pydetect_linked_courses.pydetect_modality_variants.pyresolve_api_version.pysplit_course_code.pyvalidate_completeness.py詳細
コースカタログをEducation Cloudに移行する
複数のソース(CSV、PDF、ウェブサイト)からコースカタログデータをインポートし、構造の検出とデータ品質の検証を行いながら、Learning Courseレコードを一括作成します。
対象範囲
- 対象範囲:複数ソースからの取り込み(CSV、PDF、ウェブ)、sObject REST APIを使ったLearningおよびLearningCourseの一括作成(Compositeバッチは最大200件)、関連コースの検出(講義+実習はIsLinkedOnlyを使用)、単位時間の解析(Duration/DurationUnit)、コース種別のマッピング、データ品質の検証、競合の解決、不足項目の特定と修正
- 対象外:Learning Courseレコードの個別作成(メタデータ生成スキルに委任)、Learning Programの作成(Learning.Type = 'LearningProgram'+別の子オブジェクト)、External Learningの作成(Learning.Type = 'ExternalLearning')、コースのセクション/開講科目(CourseOfferingオブジェクト)、履修登録/受講登録のワークフロー(専用スキルはまだありません。ユーザーに確認してください)、SIS同期(別の統合ワークフロー)
必須入力
作業を進める前に、次の情報を収集するか推定してください。
- 対象組織:Education Cloudが有効なアクティブなSalesforce組織
- 主なデータソース:コースカタログデータを含むCSVファイル、PDF文書、またはウェブサイトのURL
- Business Account:コースを関連付けるAcademic Operations Business Account(例:College of Engineering)
- 学期(任意):コンテキストとしてのみ使用し、Learning Courseには保存しません
追加のソース(ワークフロー中に収集):
- 不足項目(説明、種別、マッピング)を補うための追加データソース
- 複数構成のコースが検出された場合の講義と実習の対応付けファイル
- 競合解決の判断(既存コースが見つかった場合)
特に指定がない場合の既定値:
- コースのステータス:Active(LearningのIsActive = true)
- バッチサイズ:Composite API呼び出し1回あたり25レコード(エージェントの場合。APIは200件をサポートしますが、制約となるのはツール呼び出しのペイロード上限です。Step 7を参照)
- データ品質の検証:有効
- Learning.Type:'LearningCourse'(コースカタログのエントリに必須)
実行時の通信経路
まずAPIバージョンを解決してください。ハードコードしないでください。 GET /services/data/(バージョンセグメントなし)は、{version, label, url}のリストではなく、組織の最大バージョンに解決済みのエンドポイントマップを返します。これを scripts/resolve_api_version.pyに渡すと{"version": "69.0"}が得られます。トランスポートの確認を含め、以下ではすべてvXX.Xを使用してください。
次に、headless-360のディスパッチ経由でトランスポートの状態を確認します:GET /services/data/vXX.X/limits。2xxの場合は、すべての呼び出しをこの経路に通します。見つからない場合、または4xx/5xxの場合は、別のトランスポート(別のSalesforce MCP、カスタムMCP)を確認し、最初に正常と確認できたものを使用して、その旨を伝えます。正常なものが1つもない場合は停止し、接続を依頼してください。決して作り上げてはいけません。(SOQLの/queryはheadless-360でルーティング可能であることをライブテストで確認済みです。/tooling/queryと?fields=は未確認のため、describeとレコードの直接読み取りを使用してください。)
組織の認証情報を取得するために、sf CLIへ切り替えたり、シェルコマンドを実行したりしないでください。バッチの処理が遅い、またはサイズが大きすぎる場合は、トランスポートの障害ではなくバッチサイズの問題です(縮小してください。Step 7を参照)。また、CLIをシェルから実行すると、一部の環境では回避できない承認ゲートに阻まれます。組織でサポートされるバージョンより新しいバージョンを指定すると、正常なトランスポートでも404 NOT_FOUNDが返されます。その場合はGET /services/data/を再度実行し、組織の最大バージョンに下げてから再試行してください。トランスポートを切り替えてはいけません。どちらのケースについても、references/gotchas.mdを参照してください。
ワークフロー
すべてのステップを順番に実行してください。省略したり、順序を入れ替えたりしないでください。作業が進められない場合は停止し、不足している情報を確認してください。
フェーズ 1 — 主ソースの取り込み
-
データソースの種類を特定する — 主ソースが CSV、PDF、ウェブサイトの URL のいずれかを判定します。まだ指定されていない場合は、ユーザーにソースの提供を依頼します。
-
主ソースを解析する — コースデータを抽出します。
- CSV の場合:
Readツールを使用してファイルを読み込み、列を解析します - PDF の場合:
Readツールを使用してテキストを抽出します(PDF 抽出に対応)。20 ページを超える場合は、ページ範囲ごとに順番に読み取り、完全性を検証する前に解析した行を統合します —references/large-catalog-handling.md§1 を参照 - ウェブサイトの場合:
WebFetchツールを使用して HTML を取得し、コース一覧を解析します
次の項目を確認します:コースコード(識別子 — 教科略称とコース番号に分割します。手順 8/9 を参照)、コース名(Name)、単位時間(Duration + DurationUnit)、説明(Description)、コースの種類(講義/実習/試験/演習/ゼミ)、前提条件、共修条件、関連コースの指標(IsLinkedOnly)、提供形式(同じコースコードが異なる Duration/DurationUnit で繰り返される場合 —
references/large-catalog-handling.md§2 を参照)。 - CSV の場合:
-
データの完全性を検証する — 解析したレコードを完全性チェック用ヘルパーに渡します。
python3 scripts/validate_completeness.py --required "CourseCode,Name,Duration" --recommended "Description,CourseType" <<'JSON' [ { "CourseCode": "...", "Name": "...", ... }, ... ] JSONレポートには、フィールドごとの入力済み/未入力の件数が表示されます。任意項目(前提条件、共修条件、関連コース)はチェックされません。これらが未入力でも、処理が妨げられることはありません。
必須フィールドに
missing > 0がある場合は、エラーを報告して停止します。推奨フィールドに未入力がある場合は、不足項目を記録してフェーズ 2 に進みます。
フェーズ 2 — 不足項目の特定と追加ソースの依頼
-
データ品質を報告する — 入力済みのフィールドと不足しているフィールドを示す概要表を出力します。例:
Data Quality Report: • Course codes: 120/120 OK • Course names: 120/120 OK • Credit hours: 120/120 OK • Descriptions: 115/120 (5 missing) • Course types: 118/120 (2 missing) -
補足ソースを依頼する — 重要なフィールド(説明、種類など)が不足している場合は、ユーザーに「追加のソース(ウェブサイト、学部の文書、マッピングファイルなど)はありますか?」と尋ねます。
- ある場合:追加ソースを受け取り、解析して主ソースのデータと統合します
- ない場合:プレースホルダー値を使ってコースを作成するか、完全なデータがそろうまで停止して待機するかを提案します
すべての不足項目が埋まるか、不完全なデータで処理を進めることをユーザーが確認するまで、この手順を繰り返します。
教科と Business-Account の対応関係が曖昧な場合は、曖昧なケースをすべて一度に解決し(ソース文書 + 公開中のカタログ + コースのレベル/番号のパターン)、1 件ずつではなくまとめて提示します(
references/gotchas.mdを参照)。 -
複数コンポーネントのコースを検出する — コースコードを講義/実習ペアリング用ヘルパーに渡します。
python3 scripts/detect_linked_courses.py --suffixes "L,LAB" <<'JSON' [ "BIO-201", "BIO-201L", ... ] JSON返された各
{"lecture", "lab"}ペアは、複数コンポーネントのコース候補です。- ペアが見つかった場合は処理を一時停止し、次のように説明します:「講義と実習を含む複数コンポーネントのコースが X 件見つかりました。(1)独立した別々のコースとして構成するか、(2)関連コース(IsLinkedOnly フラグを設定し、学生が両方を履修する必要がある)として構成しますか?」
ユーザーが「関連コース」を選択した場合は、マッピングファイルを依頼するか、命名パターンが信頼できることを確認します。
提供形式の重複を検出する — 解析した行をコースコードごとにグループ化します。同じコードでも
Duration/DurationUnit/Descriptionが異なる場合は、重複ではなく提供形式のバリエーション(例:オンラインと対面)です。暗黙のうちに重複排除または統合してはいけません。表現方法はコースごとではなく、実行ごとに一度だけ決定します。references/large-catalog-handling.md§2 を参照してください。
フェーズ 3 — Learning および LearningCourse レコードの一括作成
-
一括作成の計画を準備する — 解析したコースレコードを一括処理用ヘルパーに渡します。Composite API では 1 回の呼び出しにつき最大 200 件のレコードを受け付けますが、エージェントのツール呼び出しペイロードには、実用上の上限(約 56KB/150 件)があります。エージェントとして実行する場合は
--batch-size 25を使用してください(呼び出し環境がより大きなペイロードに対応できることを確認できた場合に限り、最大 200 まで増やします)。python3 scripts/batch_courses.py --batch-size 25 <<'JSON' [ { "Name": "...", ... }, ... ] JSON返された配列の各要素は、1 回の Composite API 呼び出しに含めるレコード一式です。次のように出力します:「コースカタログのインポートを開始します。Composite API を使用してレコードを一括作成します。合計:X 件のコース。」 正常に動作しているディスパッチトランスポートを通じてバッチをインラインで処理できない場合は、トランスポートを切り替えるのではなく、
--batch-sizeを小さくして(例:10)再実行してください。ディスパッチとフォールバックの件数を記録し、Phase 5 のサマリーで両方を報告してください。200 コース超(8 バッチ以上):最初にバッチの総数を明示し、バッチごとに(最後だけでなく)
Batch N completeの行を出力してください。また、サーフェスに並列タスク/サブエージェント用ツールがある場合に限り、references/large-catalog-handling.md§3-4 のファンアウトルールに従ってください。その機能があると決めつけてはいけません。既定ではバッチを順次処理してください。 -
既存コースを確認する — Batch 1 を作成する前に、確認対象をこのバッチ独自のコースコードに限定してください。全件取得はしないでください(組織内に無関係なカタログエントリがすでにあっても、競合にはなりません)。
まず、このバッチのコースコードを分割してください(
scripts/split_course_code.py)。確認にはそれらの識別フィールドを使い、CipCodeは使わないでください(多くの組織では null のままです。references/gotchas.mdを参照)。Headless-360 に関する注意:SOQL
/queryは headless-360 ディスパッチツール経由でルーティング可能です(ライブテストで確認済み)。各分割済み{SubjectAbbreviation, CourseNumber}の組み合わせに完全一致するLearningCourseを照会してください(SOQL にはタプルのINがないため、各ペアを OR でつなぎます):SELECT Id, LearningId, Name, SubjectAbbreviation, CourseNumber FROM LearningCourse WHERE (SubjectAbbreviation = 'BIO' AND CourseNumber = '201') OR ...。Id が不明で、SOQL による照会もできない場合は、ユーザーに Id(または以前のインポートマニフェスト)の提示を依頼してください。Bash/CLI 経由では絶対に行わないでください。references/gotchas.mdを参照してください。結果が返された場合(つまり、このバッチ内の SubjectAbbreviation+CourseNumber の組み合わせが組織内にすでに存在する場合)は、一時停止して競合について説明してください。
- 既存コースの詳細と新しいインポートデータを並べて表示する
- 既存コースに依存関係(コース開講、履修登録)があるか確認する
- 次の選択肢を提示する:(1)既存コースを更新する、(2)既存コースをスキップする、(3)キャンセルして手動で確認する
先に進む前に、ユーザーの判断を待ってください。
-
Batch 1 を作成する — 各コースについて、次の 2 段階で処理します。
フィールドの配置(describe で確認済み):
CipCode、Duration、DurationUnit、ProviderIdは Learning のみで作成可能です。子レコードのProviderIdはINVALID_FIELD_FOR_INSERT_UPDATEエラーになります。LearningCourse が受け取るのはName、LearningId、CourseType、IsLinkedOnly、SubjectAbbreviation、CourseNumber、Description、CourseLevelDescription、FieldOfStudyです。LearningIdを介してアカウントの関連付けを継承します。リクエストボディを作成する前に describe で確認してください。CipCode は任意です。 これは連邦の CIP 分類フィールドであり、コースの識別情報ではありません。ソースに実際の CIP コードが記載され、ユーザーが確認した場合を除き、設定しないでください。
ステップ 1:Composite API で Learning(親)レコードを作成する:
POST /services/data/vXX.X/composite/sobjects { "allOrNone": false, "records": [ { "attributes": {"type": "Learning", "referenceId": "learning1"}, "Name": "<course-name>", "Type": "LearningCourse", "Description": "<description>", "ProviderId": "<account-id>", "Duration": <hours>, "DurationUnit": "Credit Hours", "IsActive": true }, ... ] }返された各 Id を取得してください(順序は
records配列と一致します)。ステップ 2 でLearningIdとして使用します。ステップ 2:Composite API で LearningCourse(子)レコードを作成する:
POST /services/data/vXX.X/composite/sobjects { "allOrNone": false, "records": [ { "attributes": {"type": "LearningCourse"}, "Name": "<course-record-name>", "LearningId": "<learning-id-from-step-1>", "SubjectAbbreviation": "<subject-prefix>", "CourseNumber": "<course-number>", "CourseType": "<parsed-course-type>", "IsLinkedOnly": false }, ... ] }各レコードについて、解析/マッピングしたコース種別から
CourseTypeを設定してください(値のマッピングは Gotchas の表を参照)。固定値を指定しないでください。実習レコードにはCourseType: "Lecture"ではなくCourseType: "Lab"を設定する必要があります。CourseType の選択リスト値(確認済み):
Lecture、Lab、Exam、Drill、Recitation。restrictedPicklist: trueは、今日送信できるのが現在の値だけであることを意味し、値の集合が固定されているという意味ではありません。 ソースの値(例:DurationUnit= "Program Points")が describe の一覧にない場合、黙って削除したり別の値にマッピングしたりしないでください。マッピング変更またはプレースホルダーに加え、メタデータ経由で選択リスト値を追加する方法も提示してください(references/gotchas.mdを参照)。進捗を出力してください:
Batch 1 complete — X Learning + X LearningCourse records created。 -
複数コンポーネントで構成されるコースを処理する — ステップ 6 でユーザーが「linked courses」を選択した場合:
CourseType: "Lecture"、IsLinkedOnly = falseを指定して講義コースを作成しますCourseType: "Lab"、IsLinkedOnly = trueを指定してラボコースを作成しますdetect_linked_courses.pyのペア(またはユーザーが指定したマッピングファイル)を使って、どのラボがリンク専用かを判定しますX linked lab courses created (IsLinkedOnly = true)と出力します
-
残りのバッチで繰り返す — すべてのコースが作成されるまで、バッチ 2、バッチ 3 などについてステップ 8~10 を続けます。
フェーズ 4 — データ品質の検証と競合の解決
-
不完全なレコードをスキャンする — すべてのバッチが完了したら、作成レスポンスから取得した Id を使って、新しく作成した各レコードを読み戻します:
GET /services/data/vXX.X/sobjects/Learning/<id> GET /services/data/vXX.X/sobjects/LearningCourse/<id>Headless-360 に関する注意:SOQL
/queryは headless-360 のディスパッチツール経由でルーティングできます(実際のテストで確認済み)。CreatedDate = TODAYクエリを使えば、取得した Id を使って各レコードを直接 GET する代わりにできます。完全性チェックのヘルパーは、sObject ごとに別々に実行します。
CourseTypeはLearningCourseにのみ存在し、DescriptionはLearningにのみ存在します。両方の形状を 1 回の呼び出しにまとめると、すべての親レコードでCourseTypeが欠落しているという誤検知が発生します:python3 scripts/validate_completeness.py --required "Name" --recommended "Description" <<'JSON' [ { "Name": "...", "Description": "...", ... }, ... ] # Learning の読み戻しのみ JSON--recommended "CourseType"を指定して、LearningCourse の読み戻しに対しても繰り返します。説明が欠落しているコース(Learning レポート)または種類が欠落しているコース(LearningCourse レポート)を特定します。ステップ 4 と同じ形式でデータ品質レポートを出力します。 -
データの競合を検出する — 複数の情報源で同じコースの値が異なる場合は、競合として報告します。例:「CHEM-301:ウェブサイトでは X、PDF では Y と記載されています。どちらを使用しますか?」ユーザーの判断を待ち、競合しているコースを更新します。
-
不足情報を補完する — ユーザーが不足情報を埋めるための追加の情報源を提示した場合は、それを解析して不完全なコースを更新します:
PATCH /services/data/vXX.X/sobjects/Learning/<id> {"Description": "..."} PATCH /services/data/vXX.X/sobjects/LearningCourse/<id> {"CourseType": "<parsed-course-type>"}
フェーズ 5 — 概要と検証
-
インポート概要を生成する — 最終レポートを出力します:
コースカタログのインポートが完了しました! 概要: • X 件の Learning レコードと X 件の LearningCourse レコードを作成しました • Y 件の単独コース、Z 件のリンク済みラボコース(IsLinkedOnly) • すべてのコースを <Business Account name> にリンクし、Active に設定しました 使用したデータソース:1. <primary> 2. <supplementary 1> 3. <supplementary 2>補足ソースから不足分を補完した場合(手順 5/14 を実行した場合)、最終レポートには次の内容もこの順序で必ず含めてください:
- データ品質レポート — 「初期データ(
<primary source>から)」と「補足ソースの適用後(<supplementary source>)」の 2 つの表。それぞれに、手順 4 のすべてのフィールドと、その入力済み数/総数を記載します。最終的な件数だけでなく、適用前と適用後の両方を示します。 - 不足分の補完 — フィールド別(例:「説明が不足していたコース(
<source>から補完)」)に分類し、補完したコースすべてを、受け取った値とともに番号付きリストで示します。補完したフィールドごとにリストを 1 つ作成します。
- データ品質レポート — 「初期データ(
-
検証を提案する — ユーザーに次のように尋ねます:「特定のコースを照会して、データを検証しますか? たとえば、LearningCourse 子レコードとともにコースを表示したり、残っているデータ品質の問題を確認したりできます。」
-
実行中のユーザーが、作成したデータを実際に参照できることを確認する — 問題があると指摘するだけでなく、実際に確認します(解決中のユーザー + FLS/RecordTypeVisibility のパターンについては
references/gotchas.mdを参照)。対象は、処理したすべてのオブジェクト(Person Accounts が関係する場合はLearning、LearningCourse、Account)です。問題と該当するプロファイルを明示し、具体的な修正方法を提案したうえで、Profile/PermissionSetメタデータを書き込む前にユーザーの確認を待ちます。OWD/共有設定がデフォルトで非公開の場合、その設定は組織全体に適用され、ユーザーごとの設定ではありません。変更を提案せず、その旨を伝えてください。
ルール/制約
| 制約 | 理由 |
|---|---|
| Composite API を使用し、エージェントによる実行では 1 回の呼び出しにつき 25 件のレコードをまとめて処理します(API は最大 200 件に対応) | H360 MCP の sObject REST 標準では、150 レコードのペイロード(約 56 KB)は一般的なツール呼び出しの上限を超えます |
| 先に Learning を作成し、その後に LearningCourse を作成します | LearningCourse.LearningId で親レコードが先に存在している必要があります |
| 常に Learning.Type = 'LearningCourse' を設定します | Learning は多態型であり、Type でコースとプログラムを区別します |
| 作成前に、既存のコースを必ず照会します | 重複を防ぎ、競合がある場合にユーザーが判断できるようにします |
| 複数のコンポーネントで構成されるコースについては、確認のために処理を一時停止します | IsLinkedOnly フラグは登録の動作に影響するため、ユーザーが構成を確認する必要があります |
| 次に進む前に、データ品質の不足を報告します | 組織に確定内容を書き込む前に、ユーザーが不足のないデータを提供できるようにします |
| データが競合する場合は、ユーザーの判断を待ちます | どのソースが正しいかをエージェントが判断することはできません |
| 不足しているデータを捏造しません | プレースホルダー値を使用する場合は、ユーザーの明示的な承認が必要です |
| 親 Learning のみに ProviderId(Business Account)を設定します | Learning では作成可能ですが、LearningCourse では作成できません。手順 9 のフィールド配置に関する注記を参照してください |
| 既定では、すべてのコースのステータスを Active(IsActive = true)に設定します | カタログ検索/登録に表示するには Active である必要があります |
| 組織への呼び出しには CLI コマンドではなく sObject REST API を使用します | H360 MCP スキルには、組織にアクセスするための実行環境がありません。scripts/ ヘルパーはローカルでデータを変換するもの(組織への呼び出しは行いません)です。Bash ツールが利用できない場合は、処理を省略せず、同等の確認をその場で実行してください |
| 特定のツールに依存しない表現を使用します | このスキルは Claude Code 以外にも、複数の AI サーフェスで利用されます |
コースが 200 件を超える場合、またはソースページが 20 件以上ある場合は、場当たり的に分割せず、references/large-catalog-handling.md に従います | 場当たり的な分割では、ページの分断によるデータ損失や重複作成が発生するおそれがあります |
注意事項
既知の失敗要因と解決方法(解析時の問題、Business Account の欠落、選択リストのマッピング、Composite の一部失敗など)については、references/gotchas.md を参照してください。
出力の想定
成果物:
- 作成された X 件の Learning レコード(Type = 'LearningCourse')
- 作成された X 件の LearningCourse 子レコード(LearningId 参照)
- フィールドごとの完全性を示すデータ品質レポート
- 使用したすべてのデータソースを記載したインポート概要
- 任意:レコードの直接読み取りまたは SOQL
/queryによるコースの検証(どちらも headless-360 経由でルーティングできます)
ファイルは作成されません。すべての出力は、sObject REST API 経由で作成された組織のレコードです。
スキル間の連携
| 必要な作業 | 委任先 |
|---|---|
| 個別の Learning Course を手動で作成する | Platform メタデータ生成スキル |
| Learning Program を作成する | 専用スキルはまだありません。ユーザーに確認してください |
| コースのセクション/提供を設定する | 専用スキルはまだありません。ユーザーに確認してください |
| 登録資格ルールを設定する | 専用スキルはまだありません。ユーザーに確認してください |
参照ファイルの一覧
references/gotchas.md— 既知の失敗パターンとその解決方法references/gotchas-detail.md— 発生頻度の低い注意事項。gotchas.mdからリンクされていますreferences/large-catalog-handling.md— 200 件を超えるコース、または 20 ページを超えるソースドキュメントのルール:ページ分割読み取り、提供形態が重複するコースの検出、チェックポイント処理、条件付き並列展開(手順 2、6、7)scripts/resolve_api_version.py— headless-360 エンドポイントマップから組織の最大 API バージョンを抽出します(実行トランスポート)scripts/validate_completeness.py— 必須/推奨フィールドの有無をチェックします(手順 3、12)scripts/detect_linked_courses.py— 講義/実習コースのコードの組み合わせを検出します(手順 6)scripts/detect_modality_variants.py— Duration/DurationUnit/Description が異なる CourseCode 行にフラグを付けます。large-catalog-handling.md§2 向けですscripts/batch_courses.py— Composite API のバッチ分割(手順 7)scripts/split_course_code.py— SubjectAbbreviation/CourseNumber の分割(手順 9)