面談の記録をExcelで作っていると、前回の話や、その後どうなったかを探し直すことになる。フォローが必要な内容を別の課題表へ転記する運用も、漏れや重複が起きやすい。
そこで、面談結果と次回へのフォローアップをプリザンターで管理する構成を考えた。閲覧するのは面談担当リーダーと管理担当者だけ。メンバー本人にはシステム上の閲覧権限を与えない前提だ。
注意:掲載するサイトパッケージは公開仕様とエクスポート例に合わせて作成したテンプレート。実際のPleasanterからエクスポートしたものではなく、対象バージョンへの実機インポート試験も未実施。テスト環境で動作確認し、権限を設定するまで実データを投入しないこと。
サンプルデータについて:記事内の組織名、グループ名、ユーザー名、ID、URL、ファイルパスはすべて架空の例です。実在する組織の内部名称や実環境の接続情報は含みません。
example.invalidは例示専用の予約ドメインです。
┌────────────────────┐
│ 1on1面談管理 │
│ (フォルダ) │
└──────────┬─────────┘
│
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌────────────┐ ┌────────────┐
│面談対象者台帳│ │1on1面談記録 │ │フォロー課題 │
│1人1レコード│───▶│1面談1レコード│───▶│1課題1レコード│
└──────────┘ └────────────┘ └────────────┘
| サイト | テーブル種別 | 役割 |
|---|---|---|
| 面談対象者台帳 | 記録テーブル | 氏名、所属、役割、育成方針、面談リーダー |
| 1on1面談記録 | 記録テーブル | 面談内容、本人の希望、リーダー所見、上位確認 |
| フォローアップ課題 | 期限付きテーブル | 誰が・いつまでに・何をするか |
上の矢印はテーブル間の参照関係を表している。実装では面談記録の ClassA が面談対象者台帳を参照し、フォローアップ課題の ClassA が面談記録を参照する。
1on1面談を保存
│
├─ フォローアップ不要 ──▶ 面談記録だけを保存
│
└─ フォローアップ必要
│
▼
同じ面談を参照する課題があるか確認
│
┌────┴────┐
│ │
なし あり
│ │
▼ ▼
課題を1件作成 何もしない
承認や上位確認のステータス遷移は設けない。面談担当者が記録を保存した時点で、CheckA(フォローアップ必要)がオンならサーバスクリプトが動く。関連面談IDで既存課題を検索してから作成するため、同じ面談を再保存しても課題は重複しない。
| Pleasanterの項目名 | 表示名 |
|---|---|
Title |
ユーザ名 |
ClassA、ClassB |
所属チーム、役割 |
Owner |
面談担当リーダー |
ClassC |
在籍状況 |
DescriptionA、DescriptionB |
育成目標、担当業務 |
DateA、CheckA |
面談開始日、面談対象 |
| Pleasanterの項目名 | 表示名 |
|---|---|
Title / ClassA / DateA |
面談件名 / 面談対象者台帳のユーザ名 / 面談日 |
Owner |
面談リーダー |
ClassB / ClassC |
面談区分 / 本人申告の業務状況 |
DescriptionA |
現在の業務状況 |
DescriptionB |
困っていること・課題 |
DescriptionC |
本人の意向・希望 |
DescriptionD |
リーダー所見 |
DescriptionE |
双方で合意した対応事項 |
DescriptionF |
次回確認事項 |
DateB |
次回面談予定日 |
CheckA |
フォローアップ必要 |
ClassA は面談対象者台帳(サンプルサイトID 9700001)への参照項目であり、自由入力ではない。候補には台帳の Title(表示名:ユーザ名)が表示される。
本人が話した内容と、リーダー側の見解は別の欄にする。 読み返したときに、誰の認識だったのかが分からなくなることを防ぎたい。
| Pleasanterの項目名 | 表示名 |
|---|---|
Title |
課題・対応事項 |
ClassA / ClassB |
関連面談記録 / 対象メンバー |
Owner / ClassC |
担当リーダー / 対応主体 |
StartTime / CompletionTime |
対応開始日 / 対応期限 |
Status / ProgressRate |
対応状況 / 進捗 |
Body / DescriptionA |
対応内容 / 結果 |
ここでは標準の CompletionTime を 対応期限 として使っている。実際の完了日を追跡したい場合は別項目を追加する。
site-package.json を指定し、サイト・レコード・項目のアクセス制御、通知、リマインダーの取り込みは OFF にする。サイトパッケージは新しいサイトを作る機能であり、既存サイトの上書きには使えない。トップのフォルダが継承する権限にも注意が必要だ。
面談は機密情報を含む。今回はインポート先のユーザーIDやグループIDが分からないため、サイトパッケージに権限設定を埋め込んでいない。導入先で設定する必要がある。
| 利用者 | 面談対象者台帳 | 面談記録 | フォロー課題 |
|---|---|---|---|
SAMPLE_1ON1_LEADER |
担当分のみ読取 | 作成+担当記録の読取・更新 | 作成+担当分の読取・更新 |
SAMPLE_1ON1_ADMIN |
管理 | 管理 | 管理 |
| 一般メンバー | 権限なし | 権限なし | 権限なし |
設定の順番
サイトパッケージの面談記録テーブルには、次のサーバスクリプトを ServerScripts として同梱している。
フォローアップ課題を自動作成create_follow_up_issueFOLLOW_UP_SITE_ID:サイトパッケージのID変換マーカーにより、インポート先の「フォローアップ課題」テーブルのサイトIDへ自動変換インポート後、面談記録テーブルの[管理]→[テーブルの管理]→[サーバスクリプト]を開き、スクリプトが有効な状態で登録され、作成後・更新後にチェックが付いていることを確認する
// 面談記録の作成後・更新後に実行する。
// CheckA がオンで、同じ面談を参照する課題が存在しない場合だけ1件作成する。
// @siteid list start@
const FOLLOW_UP_SITE_ID = 9700003;
// @siteid list end@
const meetingId = String(model.ResultId);
if (!model.CheckA) {
return;
}
const view = {
View: {
ColumnFilterHash: {
ClassA: JSON.stringify([meetingId])
}
}
};
const existing = Array.from(
items.Get(FOLLOW_UP_SITE_ID, JSON.stringify(view))
);
if (existing.length > 0) {
return;
}
const followUp = items.NewIssue();
followUp.Title = model.Title + ' のフォローアップ';
followUp.ClassA = meetingId;
followUp.ClassB = String(model.ClassA);
followUp.Owner = model.Owner;
followUp.ClassC = 'リーダー';
followUp.StartTime = model.DateA;
followUp.Body = model.DescriptionE || model.DescriptionF || '対応内容を入力してください。';
followUp.Status = 100;
if (!items.Create(FOLLOW_UP_SITE_ID, followUp)) {
context.Log('フォローアップ課題の作成に失敗しました。面談ID=' + meetingId);
}
この処理は課題の自動作成だけを行う。CheckA を後からオフにしても、既に作った課題は自動削除しない。また、面談内容を更新しても既存課題へは上書きしない。履歴保全と意図しない削除を避けるためである。
サーバスクリプトの items.Create と items.Get は実行ユーザーのアクセス権に従う。権限不足の場合は作成できないため、運用開始前にリーダー権限で必ずテストする。
ユーザ名 が候補として表示され、自由入力になっていない。フォローアップ必要 をオフにした面談では課題が作られない。フォローアップ必要 をオンにした面談では関連課題が1件作られる。Python 3.10以上で次を実行する。
python validate-package.py
この検査は形式と意図した状態遷移をチェックするもので、実機インポートの成功を保証しない。
ファイル:site-package.json(省略なし)
{
"HeaderInfo": {
"AssemblyVersion": "1.5.8.1",
"BaseSiteId": 9700000,
"Server": "https://pleasanter.example.invalid",
"CreatorName": "Pleasanter 1on1 sample generator (fictional data)",
"PackageTime": "2026-10-22T00:00:00+09:00",
"Convertors": [
{
"SiteId": 9700000,
"SiteTitle": "1on1面談管理",
"ReferenceType": "Sites",
"IncludeData": false,
"Order": "[9700001,9700002,9700003]"
},
{
"SiteId": 9700001,
"SiteTitle": "面談対象者台帳",
"ReferenceType": "Results",
"IncludeData": false
},
{
"SiteId": 9700002,
"SiteTitle": "1on1面談記録",
"ReferenceType": "Results",
"IncludeData": false
},
{
"SiteId": 9700003,
"SiteTitle": "フォローアップ課題",
"ReferenceType": "Issues",
"IncludeData": false
}
],
"IncludeSitePermission": false,
"IncludeRecordPermission": false,
"IncludeColumnPermission": false,
"IncludeNotifications": false,
"IncludeReminders": false
},
"Sites": [
{
"TenantId": 1,
"SiteId": 9700000,
"Title": "1on1面談管理",
"SiteName": "oneonone_9700000",
"SiteGroupName": "",
"Body": "",
"GridGuide": "",
"EditorGuide": "",
"CalendarGuide": "",
"CrosstabGuide": "",
"GanttGuide": "",
"BurnDownGuide": "",
"TimeSeriesGuide": "",
"KambanGuide": "",
"ImageLibGuide": "",
"ReferenceType": "Sites",
"ParentId": 0,
"InheritPermission": 9700000,
"SiteSettings": {
"Version": 1.017,
"ReferenceType": "Sites",
"NoDisplayIfReadOnly": false
},
"Publish": false,
"DisableCrossSearch": true,
"Comments": []
},
{
"TenantId": 1,
"SiteId": 9700001,
"Title": "面談対象者台帳",
"SiteName": "oneonone_9700001",
"SiteGroupName": "",
"Body": "",
"GridGuide": "",
"EditorGuide": "",
"CalendarGuide": "",
"CrosstabGuide": "",
"GanttGuide": "",
"BurnDownGuide": "",
"TimeSeriesGuide": "",
"KambanGuide": "",
"ImageLibGuide": "",
"ReferenceType": "Results",
"ParentId": 9700000,
"InheritPermission": 9700001,
"SiteSettings": {
"Version": 1.017,
"ReferenceType": "Results",
"GridColumns": [
"ResultId",
"Title",
"ClassA",
"ClassB",
"Owner",
"ClassC",
"CheckA"
],
"EditorColumnHash": {
"General": [
"ResultId",
"Title",
"ClassA",
"ClassB",
"Owner",
"ClassC",
"DescriptionA",
"DescriptionB",
"DateA",
"CheckA"
]
},
"Columns": [
{
"ColumnName": "Title",
"LabelText": "ユーザ名",
"ValidateRequired": true
},
{
"ColumnName": "ClassA",
"LabelText": "所属チーム",
"ChoicesText": "サンプルチームA\nサンプルチームB\nその他"
},
{
"ColumnName": "ClassB",
"LabelText": "役割",
"ChoicesText": "新人\n一般\nサブリーダー\nリーダー"
},
{
"ColumnName": "Owner",
"LabelText": "面談担当リーダー"
},
{
"ColumnName": "ClassC",
"LabelText": "在籍状況",
"ChoicesText": "在籍\n異動\n退職"
},
{
"ColumnName": "DescriptionA",
"LabelText": "育成目標"
},
{
"ColumnName": "DescriptionB",
"LabelText": "担当業務"
},
{
"ColumnName": "DateA",
"LabelText": "面談開始日",
"GridFormat": "Ymd"
},
{
"ColumnName": "CheckA",
"LabelText": "面談対象"
}
],
"ImportEncoding": "UTF-8",
"NoDisplayIfReadOnly": false
},
"Publish": false,
"DisableCrossSearch": true,
"Comments": []
},
{
"TenantId": 1,
"SiteId": 9700002,
"Title": "1on1面談記録",
"SiteName": "oneonone_9700002",
"SiteGroupName": "",
"Body": "",
"GridGuide": "",
"EditorGuide": "",
"CalendarGuide": "",
"CrosstabGuide": "",
"GanttGuide": "",
"BurnDownGuide": "",
"TimeSeriesGuide": "",
"KambanGuide": "",
"ImageLibGuide": "",
"ReferenceType": "Results",
"ParentId": 9700000,
"InheritPermission": 9700002,
"SiteSettings": {
"Version": 1.017,
"ReferenceType": "Results",
"GridColumns": [
"ResultId",
"Title",
"ClassA",
"DateA",
"Owner",
"ClassB",
"CheckA"
],
"EditorColumnHash": {
"General": [
"ResultId",
"Title",
"ClassA",
"DateA",
"Owner",
"ClassB",
"ClassC",
"DescriptionA",
"DescriptionB",
"DescriptionC",
"DescriptionD",
"DescriptionE",
"DescriptionF",
"DateB",
"CheckA"
]
},
"Columns": [
{
"ColumnName": "Title",
"LabelText": "面談件名",
"ValidateRequired": true
},
{
"ColumnName": "ClassA",
"LabelText": "面談対象者",
"ChoicesText": "[[9700001]]",
"ValidateRequired": true
},
{
"ColumnName": "DateA",
"LabelText": "面談実施日",
"GridFormat": "Ymd",
"ValidateRequired": true
},
{
"ColumnName": "Owner",
"LabelText": "面談リーダー",
"ValidateRequired": true
},
{
"ColumnName": "ClassB",
"LabelText": "面談区分",
"ChoicesText": "定期面談\n臨時面談\n育成面談\nキャリア面談"
},
{
"ColumnName": "ClassC",
"LabelText": "本人申告の業務状況",
"ChoicesText": "順調\n普通\n支援希望"
},
{
"ColumnName": "DescriptionA",
"LabelText": "現在の業務状況"
},
{
"ColumnName": "DescriptionB",
"LabelText": "困っていること・課題"
},
{
"ColumnName": "DescriptionC",
"LabelText": "本人の意向・希望"
},
{
"ColumnName": "DescriptionD",
"LabelText": "リーダー所見"
},
{
"ColumnName": "DescriptionE",
"LabelText": "双方で合意した対応事項"
},
{
"ColumnName": "DescriptionF",
"LabelText": "次回確認事項"
},
{
"ColumnName": "DateB",
"LabelText": "次回面談予定日",
"GridFormat": "Ymd"
},
{
"ColumnName": "CheckA",
"LabelText": "フォローアップ必要"
}
],
"PermissionForCreating": {
"Owner": 5
},
"PermissionForUpdating": {
"Owner": 5
},
"ImportEncoding": "UTF-8",
"NoDisplayIfReadOnly": false,
"ServerScripts": [
{
"Id": 1,
"Title": "フォローアップ課題を自動作成",
"Name": "create_follow_up_issue",
"AfterCreate": true,
"AfterUpdate": true,
"Body": "// 面談記録の作成後・更新後に実行する。\n// CheckA がオンで、同じ面談を参照する課題が存在しない場合だけ1件作成する。\n// @siteid list start@\nconst FOLLOW_UP_SITE_ID = 9700003;\n// @siteid list end@\nconst meetingId = String(model.ResultId);\n\nif (!model.CheckA) {\n return;\n}\n\nconst view = {\n View: {\n ColumnFilterHash: {\n ClassA: JSON.stringify([meetingId])\n }\n }\n};\n\nconst existing = Array.from(\n items.Get(FOLLOW_UP_SITE_ID, JSON.stringify(view))\n);\n\nif (existing.length > 0) {\n return;\n}\n\nconst followUp = items.NewIssue();\nfollowUp.Title = model.Title + ' のフォローアップ';\nfollowUp.ClassA = meetingId;\nfollowUp.ClassB = String(model.ClassA);\nfollowUp.Owner = model.Owner;\nfollowUp.ClassC = 'リーダー';\nfollowUp.StartTime = model.DateA;\nfollowUp.Body = model.DescriptionE || model.DescriptionF || '対応内容を入力してください。';\nfollowUp.Status = 100;\n\nif (!items.Create(FOLLOW_UP_SITE_ID, followUp)) {\n context.Log('フォローアップ課題の作成に失敗しました。面談ID=' + meetingId);\n}"
}
],
"ServerScriptsAllDisabled": false
},
"Publish": false,
"DisableCrossSearch": true,
"Comments": []
},
{
"TenantId": 1,
"SiteId": 9700003,
"Title": "フォローアップ課題",
"SiteName": "oneonone_9700003",
"SiteGroupName": "",
"Body": "",
"GridGuide": "",
"EditorGuide": "",
"CalendarGuide": "",
"CrosstabGuide": "",
"GanttGuide": "",
"BurnDownGuide": "",
"TimeSeriesGuide": "",
"KambanGuide": "",
"ImageLibGuide": "",
"ReferenceType": "Issues",
"ParentId": 9700000,
"InheritPermission": 9700003,
"SiteSettings": {
"Version": 1.017,
"ReferenceType": "Issues",
"GridColumns": [
"IssueId",
"Title",
"ClassA",
"ClassB",
"Owner",
"Status",
"StartTime",
"CompletionTime",
"ProgressRate"
],
"EditorColumnHash": {
"General": [
"IssueId",
"Title",
"ClassA",
"ClassB",
"Owner",
"ClassC",
"StartTime",
"CompletionTime",
"Status",
"ProgressRate",
"Body",
"DescriptionA"
]
},
"Columns": [
{
"ColumnName": "Title",
"LabelText": "課題・対応事項",
"ValidateRequired": true
},
{
"ColumnName": "ClassA",
"LabelText": "関連する面談記録",
"ChoicesText": "[[9700002]]"
},
{
"ColumnName": "ClassB",
"LabelText": "対象メンバー",
"ChoicesText": "[[9700001]]"
},
{
"ColumnName": "Owner",
"LabelText": "担当リーダー"
},
{
"ColumnName": "ClassC",
"LabelText": "対応主体",
"ChoicesText": "本人\nリーダー\nその他"
},
{
"ColumnName": "StartTime",
"LabelText": "対応開始日",
"GridFormat": "Ymd",
"EditorFormat": "Ymd"
},
{
"ColumnName": "CompletionTime",
"LabelText": "対応期限",
"GridFormat": "Ymd",
"EditorFormat": "Ymd"
},
{
"ColumnName": "Body",
"LabelText": "対応内容"
},
{
"ColumnName": "DescriptionA",
"LabelText": "結果・確認事項"
},
{
"ColumnName": "Status",
"LabelText": "対応状況",
"ChoicesText": "100,未着手\n200,対応中\n900,完了",
"DefaultValue": "100"
}
],
"PermissionForCreating": {
"Owner": 5
},
"PermissionForUpdating": {
"Owner": 5
},
"ImportEncoding": "UTF-8",
"NoDisplayIfReadOnly": false
},
"Publish": false,
"DisableCrossSearch": true,
"Comments": []
}
],
"Data": [],
"Permissions": [],
"PermissionIdList": {
"DeptIdList": [],
"GroupIdList": [],
"UserIdList": []
}
}
ファイル:validate-package.py(省略なし)
#!/usr/bin/env python3
# UTF-8 / Python 3.10+ / 追加依存なし
import json
import sys
from pathlib import Path
EXPECTED = {"Sites": 1, "Results": 2, "Issues": 1}
REMOVED_MEETING_COLUMNS = {
"Manager", "DescriptionG", "DateC", "DateD", "ClassD"
}
def main():
path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).with_name("site-package.json")
data = json.loads(path.read_text(encoding="utf-8-sig"))
assert data["HeaderInfo"]["IncludeSitePermission"] is False, "権限を同梱しない方針"
assert data["Data"] == [], "実データを含めないこと"
ids = [site["SiteId"] for site in data["Sites"]]
assert len(ids) == 4 and len(set(ids)) == 4, "サイトIDの重複"
for name, count in EXPECTED.items():
assert sum(s["ReferenceType"] == name for s in data["Sites"]) == count, name
meetings = next(s for s in data["Sites"] if s["Title"] == "1on1面談記録")
conf = meetings["SiteSettings"]
assert not conf.get("Processes"), "承認プロセスを含めないこと"
scripts = conf.get("ServerScripts", [])
assert len(scripts) == 1, "サーバスクリプトを1件含めること"
script = scripts[0]
assert script.get("AfterCreate") is True, "作成後に実行すること"
assert script.get("AfterUpdate") is True, "更新後に実行すること"
assert "items.Create" in script.get("Body", ""), "課題作成処理が必要"
assert "// @siteid list start@" in script.get("Body", ""), "サイトID変換開始マーカーが必要"
assert "// @siteid list end@" in script.get("Body", ""), "サイトID変換終了マーカーが必要"
names = {c["ColumnName"] for c in conf.get("Columns", [])}
assert "CheckA" in names, "フォローアップ必要項目が必要"
assert not (names & REMOVED_MEETING_COLUMNS), "上位確認用項目を含めないこと"
for site in data["Sites"]:
column_names = [c["ColumnName"] for c in site["SiteSettings"].get("Columns", [])]
assert len(column_names) == len(set(column_names)), "項目名重複: " + site["Title"]
print("PASS: JSON構造・テーブル・サーバスクリプト・上位確認項目の除外(静的検査)")
print("NOTE: サーバスクリプト、実機インポート、権限、動作は実環境で要検証")
if __name__ == "__main__":
main()