n8n-io/n8n · error · CannotDetermineEntityError

Cannot ${operation}, given value must be instance of entity

Error message

Cannot ${operation}, given value must be instance of entity class, instead object literal is given. Or you must specify an entity target to method call.

What it means

`EntityPersistExecutor` resolves the entity target as `this.target ?? entity.constructor`. When you call `manager.save(plainObj)` (no bound target) and `plainObj` is a `{}`-literal, `entity.constructor` is `Object` — meaning TypeORM cannot tell which entity metadata to use. It throws `CannotDetermineEntityError` rather than guessing. Typed repositories have a target so they never hit this.

Source

Thrown at packages/@n8n/typeorm/src/persistence/EntityPersistExecutor.ts:76

		}

		try {
			// collect all operate subjects
			const entities: ObjectLiteral[] = Array.isArray(this.entity) ? this.entity : [this.entity];
			const entitiesInChunks =
				this.options && this.options.chunk && this.options.chunk > 0
					? OrmUtils.chunk(entities, this.options.chunk)
					: [entities];

			// console.time("building subject executors...");
			const executors = await Promise.all(
				entitiesInChunks.map(async (entities) => {
					const subjects: Subject[] = [];

					// create subjects for all entities we received for the persistence
					entities.forEach((entity) => {
						const entityTarget = this.target ? this.target : entity.constructor;
						if (entityTarget === Object) throw new CannotDetermineEntityError(this.mode);

						let metadata = this.connection
							.getMetadata(entityTarget)
							.findInheritanceMetadata(entity);

						subjects.push(
							new Subject({
								metadata,
								entity: entity,
								canBeInserted: this.mode === 'save',
								canBeUpdated: this.mode === 'save',
								mustBeRemoved: this.mode === 'remove',
								canBeSoftRemoved: this.mode === 'soft-remove',
								canBeRecovered: this.mode === 'recover',
							}),
						);
					});

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Pass the entity class as the target argument: `await manager.save(User, plainObject)`.
  2. Instantiate the entity first: `manager.save(Object.assign(new User(), dto))`.
  3. Use the typed `repository.save(...)` from `dataSource.getRepository(User)`, which already carries the target.

Example fix

// before — plain object, no target → error
await dataSource.manager.save({ name: 'Alice', email: 'a@x.com' });

// after — explicit target
await dataSource.manager.save(User, { name: 'Alice', email: 'a@x.com' });
// or
const user = Object.assign(new User(), { name: 'Alice', email: 'a@x.com' });
await dataSource.manager.save(user);
Defensive patterns

Strategy: type-guard

Validate before calling

// Ensure plain objects are paired with an entity target before persisting
function isEntityInstance<T>(o: T, ctor: new () => T): o is T {
  return o instanceof ctor;
}
async function safeSave<T>(manager: EntityManager, ctor: new () => T, input: Partial<T>) {
  const entity = Object.assign(new ctor(), input);
  return manager.save(entity); // safe: target is ctor
}

Type guard

function isPlainLiteral(o: unknown): o is Record<string, unknown> {
  return o !== null && typeof o === 'object' && (o as any).constructor === Object;
}

Try / catch

try { await manager.save(payload); } catch (e) { if (e instanceof CannotDetermineEntityError) { /* re-dispatch as manager.save(EntityClass, payload) */ } throw e; }

Prevention

When it happens

Trigger: Calling `dataSource.manager.save({ name: 'x' })` or `manager.remove({ id: 1 })` with a plain object literal and no first-argument entity target. Passing deserialized JSON directly (e.g. `req.body`) into `manager.save`.

Common situations: Using the bare EntityManager instead of a repository. Forgetting to pass the entity class as the first argument: `manager.save(EntityClass, dto)`.

Related errors


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