# org-sync: ② merge-master ワークフロー設計書 対象: n8n workflow `iRBAQquKr9411ENG`(Webhookトリガー、`https://n8n32.next-hd.net/webhook/org-sync-merge-master`) 記録時点: 2026-08-17(export/import方式改修後、66ノード) 関連サブワークフロー: `logic-merge-master-plan`(id: `asKvC5LSVg3q8tuU`) ## 1. 全体像 LINEWORKS社員台帳とプリザンター実ユーザーを突き合わせ、社員・組織マスタ管理テーブル(正サイト)へCreate/Update/退職処理を反映する。実行順は大きく4ブロック: ``` [A] 入力データ収集 ↓ [B] logic-merge-master-plan サブワークフロー呼び出し(マージ判定) ↓ [C] 副サイトバックアップ(全クリア→export→import) ↓ [D] 選択肢マスタ解決 → Create/Update/退職 実行(3分岐) ``` Bまでは軽い処理、Cは今回OOMでクラッシュした区間、Dが最終的な書込。**C(バックアップ)とD(本処理)は`副サイトImport`ノードを境に直列**であり、Cが終わらないとDへ進まない設計。 ## 2. ブロックA: 入力データ収集(Webhook起動 〜 副サイトSiteId取得) | ノード | 種別 | 処理 | |---|---|---| | `Webhook起動` | Webhook | POSTで起動。認証ヘッダー`X-API-Key` | | `PLEASANTER_BASE_URL_TEST取得`〜`PLEASANTER_USERTYPE_MASTER_SITE_ID_TEST取得`(4連) | Data Table Get | `workflow_config_values`から設定値を1キーずつ直列取得(複数行一括取得による後続反復実行バグの回避策) | | `lineworks_users全行取得` | Data Table Get | `lineworks_users`(619行、`rawJson`列にLINEWORKS社員データ)を`returnAll:true`で一括取得 | | `lineworks_usersまとめ` | Aggregate | 619アイテムを1アイテム(`rows`配列)に集約(後続の反復実行を防ぐ定石) | | `users配列復元` | Set | `rows.map(r => JSON.parse(r.rawJson))`で619件分の`rawJson`文字列を**全部その場でJSONパース**し`users`配列を作る | | `meta取得` | Data Table Get | `lineworks_snapshot_meta`から`fetchedAt`取得 | | `マスタ全件取得` | HTTP Request(Pagination) | `POST .../api/items/{正サイトSiteId}/get`。Offsetベースのページングで619件を`maxRequests:20`まで取得 | | `マスタ展開` | SplitOut | `Response.Data`(619件)を619アイテムに展開 | | `マスタHashフラット化` | Set | **619アイテムそれぞれに対して1回ずつ実行**(n8nの入力アイテム単位実行)。`ClassHash`等5つのHashをトップレベルへ展開マージ | | `existingItemsまとめ` | Aggregate | 619件を1アイテム(`rows`)に再集約 → `existingItems` | | `実ユーザー全件取得` | HTTP Request(Pagination) | `POST .../api/users/get`。プリザンター実ユーザー全件 | | `実ユーザー展開`→`pleasanterUsersまとめ` | SplitOut→Aggregate | 上と同じ展開→再集約パターン | | `email overrides取得`→`overridesまとめ`→`overridesマップ化` | Data Table Get→Aggregate→Set | メール重複時の解決用オーバーライド表を`{emailLocal: pleasanterUserId}`のマップに変換 | | `サブワークフロー入力組立` | Set | `users`/`existingItems`/`pleasanterUsers`/`emailOverrides`/`fetchedAt`の5項目を1オブジェクトにまとめる。**619件×2セットの配列をまるごと1つのJSONアイテムに保持**した状態でここを通過 | **この時点でメモリ上に「619件のLINEWORKS社員生データ」「619件の正サイト全カラム展開データ」「N件の実ユーザーデータ」を同時に保持している**。 ## 3. ブロックB: マージ判定(logic-merge-master-plan サブワークフロー) `logic-merge-master-plan実行`(Execute Workflow)が呼ぶサブワークフローは2ノードのみ: - `Execute Workflow Trigger` - `planMerge計算`(Codeノード。`apps/org-master-sync/src/commands/merge-master.js`の`planMerge()`をJSに移植したもの) 判定ロジック(概要): 1. LINEWORKS側`users`を1件ずつ走査 - `ClassA`(社員ID)一致 → 次点で`ClassB`(メール)ローカル部一致、で正サイト既存行(`existingItems`)を検索 - プリザンター実ユーザー(`pleasanterUsers`)をメールで突き合わせ。同一ローカル部に複数候補いる場合は`emailOverrides`で解決、無ければ`needsReview`へ積む(自動処理をスキップし要人手確認としてマーク) - 既存行あれば`updates`、無ければ`creates`へ、生年月日・入社日は「新しい値が無く旧値もダミー日付でなければ`1899/12/31`でクリアする」という日付クリア専用ロジックあり 2. 正サイトにいるがLINEWORKS側で見えなくなったプリザンター実ユーザー(`mirroredPleasanterUserIds`に含まれない)を`updates`または`creates`へ追加登録(在籍フラグ`Check062:false`) 3. 正サイトの既存行のうち`Check062:true`(LINEWORKS在籍フラグ)なのに今回のLINEWORKSスナップショットに現れない行を`retirements`へ(退職処理対象) 出力: `{creates, updates, retirements, needsReview}` の4配列。**この計算自体はn8nコンテナ内のCodeノード1回実行で完結し、比較的軽い**(619件規模のループだが単純なオブジェクト構築のみ)。 ## 4. ブロックC: 副サイトバックアップ(2026-08-17改修、OOMクラッシュ発生区間) `logic-merge-master-plan実行`の直後、メイン処理(件数集計以降)より**先に**直列で走る: ``` 副サイトSiteId取得 → 副サイト全クリア → 正サイトExport → Import用ファイル変換 → 副サイトImport → (件数集計 / needsReview展開 / 退職対象展開準備 の3分岐) ``` | ノード | 種別 | 処理 | |---|---|---| | `副サイトSiteId取得` | Data Table Get | `workflow_config_values`から`PLEASANTER_BACKUP_SITE_ID_TEST`(=1362)取得 | | `副サイト全クリア` | HTTP Request | `POST .../api/items/1362/bulkdelete`、`{All:true, PhysicalDelete:true}`。**2026-08-17の実行はここで停止しクラッシュ**(`lastNodeExecuted`) | | `正サイトExport` | HTTP Request | `POST .../api/items/{正サイトSiteId}/export`、`Export.Columns`に全62列を明示指定、`Type:"csv"`でCSV文字列を取得 | | `Import用ファイル変換` | Convert to File(`toText`) | `sourceProperty:"Response.Content"`のCSV文字列をバイナリ化(`binaryPropertyName:"data"`) | | `副サイトImport` | HTTP Request(multipart) | `POST .../api/items/1362/import`。`parameters`(JSON)と`file`(バイナリCSV)の2パートで619件をimport | **旧方式(〜2026-08-16)は`副サイトへ丸ごと反映`(`bulkupsert`、619件のJSON配列を1リクエストで送信)だった。2026-08-17にexport/importへ置き換え済み**(単体でのcurl直叩き検証では0.5秒+4秒と高速だった)。 ## 5. ブロックD: 選択肢マスタ解決 → Create/Update/退職 実行 `副サイトImport`から3方向に分岐、いずれも`logic-merge-master-plan実行`の出力(`creates`/`updates`/`needsReview`/`retirements`)を参照する。 ### 5-1. needsReview分岐 `needsReview展開`(SplitOut) → `needsReview行組立`(Set) → `needsReview保存`(Data Table `merge_needs_review`へInsert)。要人手確認の重複メール候補を記録するのみ、正サイトへの書込は無い。 ### 5-2. メイン分岐(件数集計 → Create/Update) 1. `件数集計`(Set): creates/updates/retirements/needsReviewの件数を数えるだけ 2. 役職・職級・利用権限タイプの3種マスタをそれぞれ`マスタ取得(Pagination)→展開(SplitOut)→まとめ(Aggregate)→マップ化(Set、`{Title: ResultId}`)`の同一パターンで直列に解決(件数は少数8/6/4件なので軽い) 3. `欠落タイトル抽出`(Set): creates+updatesの中で使われている役職・職級・利用権限タイプ名のうち、マスタに未登録のものを抽出 4. 役職・職級・利用権限タイプそれぞれについて`欠落展開(SplitOut)→新規作成(HTTP Request `/create`)→新規ペア組立(Set)→新規まとめ(Aggregate)`で欠落分を新規登録 5. `最終選択肢マップ完成`(Set): 既存マップ+新規登録分をマージした最終マップ(`positionMap`/`levelMap`/`userTypeMap`) 6. `Create対象Update対象結合`(Set): `creates`と`updates`を`targets`配列に結合 7. `対象展開`(SplitOut): `targets`を1件ずつに展開 8. `テスト用Limit(1件)`: **ここで初めて1件に絞られる**(現状 有効・disabled:false) 9. `選択肢フィールド解決`(Set): 役職名等の文字列を`最終選択肢マップ完成`のIDへ変換 10. `マスタCreate_Update実行`(HTTP Request): `itemId`の有無で`update`/`create`を出し分け、`ClassHash`/`NumHash`/`DateHash`/`DescriptionHash`/`CheckHash`へ振り分けて送信 ### 5-3. 退職分岐 `退職対象展開準備`(Set) → `退職対象展開`(SplitOut) → `退職者Update実行`(HTTP Request、**現状disabled:trueで無効化中**)。`Check062:false`・`ClassZ:"無"`・`DateA`更新のみの軽量Update。 ## 6. 2026-08-17の暴走事故と原因 Webhook実行が`副サイト全クリア`ノードで停止、n8n実行履歴のエラーは ``` "Workflow did not finish, possible out-of-memory issue" ``` **このワークフローの構造的な問題**: `テスト用Limit(1件)`は5-2の手順8、つまりCreate/Update実行の直前にしか効かない。それより前の全工程(ブロックA/B/C、619件のLINEWORKS展開・619件の正サイト展開・実ユーザー展開・マージ判定・バックアップのexport/import)は**「1件テスト」であっても常にフル619件規模で実行される**。n8nコンテナのメモリ制限(768MiB)に対し、以下が同時にメモリへ積み上がる: - `users配列復元`: 619件のLINEWORKS社員オブジェクト - `existingItemsまとめ`: 619件の正サイト全62列展開データ - `pleasanterUsersまとめ`: 実ユーザー全件データ - `サブワークフロー入力組立`〜`logic-merge-master-plan実行`: 上記を1つのJSONペイロードとして子ワークフローへコピー渡し - `正サイトExport`〜`副サイトImport`: 619件のCSV文字列(348KB相当)をさらに保持 **bulkupsert(旧方式)を2026-08-17にexport/import方式へ置き換えても症状(OOM)が再現した**ことから、暴走の主因は「バックアップ処理そのもの」ではなく、**ブロックA〜Cを通じて複数回複製される619件データの累積メモリ使用量**である可能性が高い。単一ステップの置き換えでは解決しない構造的課題。 ## 7. 対策の方向性(未実装・検討のみ) メモリ増設ができない制約下での案: 1. **Limitをブロックの入口側まで前倒しする**: `対象展開`直前ではなく、`マスタ全件取得`/`実ユーザー全件取得`のページング自体を絞る(ただし社員IDの絞り込み条件が必要で、Pleasanter API側の`View`条件設計が要る) 2. **ブロックCとブロックA/B/Dを分離する**: バックアップ(export/import)は本処理と論理的に独立しているため、別ワークフロー・別トリガーに切り出し、メイン処理のメモリと競合しないタイミング(例: 深夜バッチ)で走らせる 3. **619件を保持し続けない設計へ**: Aggregate→SplitOutの往復を減らし、可能な箇所はn8nの標準ページング機構やバッチ実行(`splitInBatches`)で「常に少数件だけをメモリに置く」ストリーミング型処理に書き換える(改修規模は大きい) 4. **Codeノードへの一本化**: 現状Set/Aggregate/SplitOutを多用したノーコード的組み方をしているが、ブロックA・Dの一部をCodeノード(JS一括処理)にまとめるとn8n側のアイテム管理オーバーヘッド(各ノードでアイテム配列をコピーする分のメモリ)を削減できる可能性がある いずれも設計変更を伴うため、着手前に方針をユーザーと確認すること。