n8n-io/n8n · error · Error

${turn.errorReason.message}

Error message

${turn.errorReason.message}

What it means

Thrown by the Set node validator (v3.3+) when parameters.assignments is a valid object but its inner assignments field is present and is not an array. The inner field must be the list of assignment entries.

Solutions

  1. Make parameters.assignments.assignments an array of assignment objects.
  2. If the inner list is genuinely empty, use assignments: { assignments: [] }.
  3. Omit the inner assignments key entirely if you only want the outer placeholder (undefined is tolerated).

Example fix

// before
set({
  name: 'Set X',
  assignments: { assignments: { id: '1', name: 'x', value: 1, type: 'number' } },
});

// after
set({
  name: 'Set X',
  assignments: {
    assignments: [{ id: '1', name: 'x', value: 1, type: 'number' }],
  },
});
Defensive patterns

Strategy: validation

Validate before calling

function assertAssignmentsArray(assignments: unknown): void {
  if (assignments !== undefined && !Array.isArray(assignments)) {
    throw new Error('parameters.assignments.assignments must be an array');
  }
}

// before building:
assertAssignmentsArray(params.assignments?.assignments);

Type guard

function isAssignmentList(v: unknown): v is unknown[] {
  return Array.isArray(v);
}

Prevention

When it happens

Trigger: params.assignments is a record AND assignments.assignments !== undefined AND !Array.isArray(assignments.assignments) — e.g. it is an object, string, or number.

Common situations: Nesting one level too deep (assignments.assignments = {...}); an AI builder mirrors the outer object shape inside; a typo turning the array into an object.

Related errors


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

Appendix: source

Thrown at packages/@n8n/agents/src/runtime/loop/agent-runtime.ts:923

			// Fold the just-finished turn's usage in before the abort check so a
			// stop that lands right after the model call still bills its tokens.
			totalUsage = mergeUsage(totalUsage, turn.usage);
			incrementTokenCountFromUsage(options?.executionCounter, turn.usage);
			sink.reportUsage(totalUsage);

			this.assertNotAborted(abortScope);

			lastFinishReason = turn.finishReason;
			list.addResponse(turn.newMessages);
			// The turn is now in the list; drop any retained streamed text so a later
			// abort's snapshot can't duplicate it (a stop before this point recovers it).
			sink.onTurnFolded?.();

			if (turn.aiFinishReason !== 'tool-calls') {
				// A rejected/filtered request (e.g. a provider prompt safety block)
				// surfaces as an output-less turn instead of an SDK error — throw so
				// the failure reaches the caller rather than ending the run silently.
				if (turn.errorReason) throw new Error(turn.errorReason.message);
				structuredOutput = turn.structuredOutput;
				this.emitTurnEnd(turn.newMessages, extractSettledToolCalls(turn.newMessages));
				reachedStopCondition = true;
				break;
			}

			const batch = await this.toolExecutor.iterateToolCallsConcurrent({
				...buildToolBatchContext(toolMap),
				toolCalls: turn.toolCalls,
			});
			const finalized = await finishToolBatch(batch, toolMap, iterationCount + 1);
			if (finalized.suspended) return finalized.result;

			// Emit TurnEnd after all tool calls in this iteration are processed
			this.emitTurnEnd(turn.newMessages, extractSettledToolCalls(list.responseDelta()));

			// Step boundary reached with nothing pending: durably checkpoint so a
			// crash before the next model call loses only the in-flight step.

View on GitHub (pinned to 5ac6606e81)