n8n-io/n8n · error · CannotAttachTreeChildrenEntityError

Cannot attach entity "${entityName}" to its parent. Please m

Error message

Cannot attach entity "${entityName}" to its parent. Please make sure parent is saved in the database before saving children nodes.

What it means

For closure-table trees, `ClosureSubjectExecutor` inserts the ancestor/descendant rows linking a child to its parent. For each closure-junction descendant column it reads the parent's value via `referencedColumn.getEntityValue(parent)`; if that value is falsy (no parent id), the parent was never persisted and the link cannot be written, so it throws `CannotAttachTreeChildrenEntityError`.

Source

Thrown at packages/@n8n/typeorm/src/persistence/tree/ClosureSubjectExecutor.ts:85

			);
			const childEntityIds1 = subject.metadata.primaryColumns.map((column) => {
				queryParams.push(
					column.getEntityValue(
						subject.insertedValueSet ? subject.insertedValueSet : subject.entity!,
					),
				);
				return this.queryRunner.connection.driver.createParameter(
					'child_entity_' + column.databaseName,
					queryParams.length - 1,
				);
			});

			const whereCondition = subject.metadata.closureJunctionTable.descendantColumns.map(
				(column) => {
					const columnName = escape(column.databaseName);
					const parentId = column.referencedColumn!.getEntityValue(parent);

					if (!parentId) throw new CannotAttachTreeChildrenEntityError(subject.metadata.name);

					queryParams.push(parentId);
					const parameterName = this.queryRunner.connection.driver.createParameter(
						'parent_entity_' + column.referencedColumn!.databaseName,
						queryParams.length - 1,
					);
					return `${columnName} = ${parameterName}`;
				},
			);

			await this.queryRunner.query(
				`INSERT INTO ${tableName} (${[...ancestorColumnNames, ...descendantColumnNames].join(
					', ',
				)}) ` +
					`SELECT ${ancestorColumnNames.join(', ')}, ${childEntityIds1.join(
						', ',
					)} FROM ${tableName} WHERE ${whereCondition.join(' AND ')}`,
				queryParams,

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Save the parent first and await it, then assign it to the child and save the child: `await repo.save(parent); child.parent = parent; await repo.save(child);`
  2. Use `TreeRepository.save` on the root of a fully-built tree so TypeORM persists in root-first order.
  3. Ensure the parent's primary key column is selected/loaded (not projected away).

Example fix

// before
const parent = new Category(); parent.name = 'root';
const child = new Category(); child.name = 'sub'; child.parent = parent;
await repo.save(child); // parent has no id yet → error

// after
await repo.save(parent);
child.parent = parent;
await repo.save(child);
Defensive patterns

Strategy: validation

Validate before calling

// Before saving a tree child, ensure its parent has an id
const parentPk = dataSource.getMetadata(Parent).primaryColumns.map(c => c.getEntityValue(child.parent));
if (parentPk.some(v => v == null)) {
  throw new Error('Parent has no id — save the parent before the child');
}

Type guard

function parentHasId<P>(parent: P, pk: keyof P): parent is P & Record<typeof pk, NonNullable<P[typeof pk]>> {
  return parent != null && (parent as any)[pk] != null;
}

Try / catch

try { await treeRepo.save(child); } catch (e) { if (e instanceof CannotAttachTreeChildrenEntityError) { /* await save(parent) first */ } throw e; }

Prevention

When it happens

Trigger: Saving a tree child whose parent is a brand-new entity not yet flushed, so the parent has no id. Calling `treeRepository.save(child)` after assigning `child.parent = new ParentEntity()` without saving the parent first.

Common situations: Building the whole tree in memory then saving leaf-first instead of root-first. Forgetting to await the parent's save before saving children. A parent loaded from a query whose PK column wasn't selected.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/212d6052939f2d25. Report an issue: GitHub.