nocobase/nocobase · error · Error
flowModels:move source and target must be sibling nodes unde
Error message
flowModels:move source and target must be sibling nodes under the same parent/subKey
What it means
Thrown by FlowModelRepository.move in the same-parent fast path: both source and target share a parentUid and type (subKey), but the sibling sort rows do not actually contain both nodes — sourceRow is missing or targetIndex is -1. The in-memory sort index and the tree table are out of sync.
Source
Thrown at packages/plugins/@nocobase/plugin-flow-engine/src/server/repository.ts:2058
const sourceInfo = await this.findSiblingInfo(sourceUid, transaction);
const targetInfo = await this.findSiblingInfo(targetUid, transaction);
if (!targetInfo) {
throw new Error('flowModels:move target is not attached to a parent');
}
const targetInstance = await this.model.findByPk(targetUid, { transaction });
const targetOptions = FlowModelRepository.optionsToJson(targetInstance?.get('options') || {});
const targetSubType = targetOptions.subType === 'object' ? 'object' : 'array';
if (targetInfo.parentUid === sourceUid || (await this.isAncestorOf(sourceUid, targetInfo.parentUid, transaction))) {
throw new Error('flowModels:move cycle detected');
}
if (sourceInfo?.parentUid === targetInfo.parentUid && sourceInfo.type === targetInfo.type) {
const siblingRows = await this.findSiblingSortRows(sourceInfo.parentUid, sourceInfo.type, transaction);
const sourceRow = siblingRows.find((row) => row.uid === sourceUid);
const targetIndex = siblingRows.findIndex((row) => row.uid === targetUid);
if (!sourceRow || targetIndex === -1) {
throw new Error('flowModels:move source and target must be sibling nodes under the same parent/subKey');
}
const rowsWithoutSource = siblingRows.filter((row) => row.uid !== sourceUid);
const insertIndex = rowsWithoutSource.findIndex((row) => row.uid === targetUid);
rowsWithoutSource.splice(position === 'after' ? insertIndex + 1 : insertIndex, 0, sourceRow);
await this.writeSiblingSorts(
sourceInfo.parentUid,
rowsWithoutSource.map((row) => row.uid),
transaction,
);
return await this.findModelById(sourceUid, { transaction });
}
await this.normalizeSiblingSorts(sourceInfo, transaction);
await this.normalizeSiblingSorts(targetInfo, transaction);
await this.updateModelParentOptions(sourceUid, targetInfo.parentUid, targetInfo.type, targetSubType, transaction);
await this.insertSingleNode(View on GitHub (pinned to fa42722fef)
Solutions
- Normalize sibling sorts first (e.g. call move once with sourceId === targetId, which triggers normalizeSiblingSorts and returns null), then retry the real move.
- Re-attach the affected children under the parent to rebuild both tree and sort rows.
- Add a repair routine that diffs findSiblingSortRows against findSiblingInfo and writes missing rows via writeSiblingSorts.
Example fix
// before
await repo.move({ sourceId, targetId, position: 'after' }); // throws when sort rows are stale
// after
try {
await repo.move({ sourceId, targetId, position: 'after' });
} catch (e) {
if (String(e.message).includes('must be sibling nodes')) {
await repo.move({ sourceId, targetId: sourceId, position: 'after' }); // normalize sorts
await repo.move({ sourceId, targetId, position: 'after' });
} else throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
const rows = await repo.findSiblingSortRows(parentUid, subKey);
if (!rows.some((r) => r.uid === sourceId) || !rows.some((r) => r.uid === targetId)) {
await repo.move({ sourceId, targetId: sourceId, position: 'after' }); // triggers normalizeSiblingSorts
}
await repo.move({ sourceId, targetId, position }); Try / catch
try {
await repo.move(opts);
} catch (e) {
if (String(e.message).includes('must be sibling nodes under the same parent/subKey')) {
await repo.move({ sourceId: opts.sourceId, targetId: opts.sourceId, position: 'after' });
await repo.move(opts);
} else throw e;
} Prevention
- Never edit the tree/sort tables outside repository APIs or transactions
- Rebuild sibling sorts after imports, restores, or bulk deletes
- Detect drift by diffing sort rows against findSiblingInfo periodically
When it happens
Trigger: Calling move between two nodes whose parentUid/type match per findSiblingInfo, but whose sort rows were never written (corrupt sort table), were written with different uids (case/whitespace mismatch), or were deleted concurrently.
Common situations: Manual DB edits or imports that populate the tree table but skip the sibling sort rows; a crash between writing tree paths and sort rows outside one transaction; duplicate uid entries with differing trailing whitespace.
Related errors
- Cycle detected in ${collectionName}: ${cyclePath.join(' -> '
- Cannot set a descendant node as the parent node
- flowModels:attach subKey '${subKey}' already exists on paren
- flowModels:attach position target is not a sibling under the
- flowModels:move target is not attached to a parent
AI-assisted analysis of nocobase/nocobase@fa42722fef (2026-09-01).
Data as JSON: /api/errors/3a1fd4a630552923.
Report an issue: GitHub.