n8n-io/n8n · critical · Error

Unknown role: ${msg.role as string}

Error message

Unknown role: ${msg.role as string}

What it means

`toAiMessageList` converts persisted n8n agent messages into AI SDK `ModelMessage`s for the next LLM call. It handles roles `system`, `user`, `assistant`, and `tool` (legacy). The `default` branch throws when `msg.role` is none of these — meaning a message with an unrecognized role was loaded from the database or memory store. This indicates data corruption or a forward-incompatible message format.

Source

Thrown at packages/@n8n/agents/src/runtime/model/messages.ts:445

				const assistantMsg: ModelMessage = msg.providerOptions
					? { ...assistantBase, providerOptions: msg.providerOptions }
					: assistantBase;
				transformedMessages.push(assistantMsg);
			}
			if (resultMessages.length > 0) {
				transformedMessages.push(...resultMessages);
			}

			return transformedMessages;
		}

		case 'tool': {
			// Legacy role: 'tool' messages (from old DB rows). Don't emit them.
			return [];
		}

		default:
			throw new Error(`Unknown role: ${msg.role as string}`);
	}
}

/** Convert n8n Messages to AI SDK ModelMessages for passing to stream/generateText. */
export function toAiMessages(messages: Message[]): ModelMessage[] {
	return messages.flatMap(toAiMessageList);
}

/**
 * Convert AI SDK ModelMessages to n8n AgentMessages.
 *
 * This is a stateful walk: when a role:'tool' ModelMessage is encountered,
 * the matching tool-call block on the preceding assistant message is mutated
 * to 'resolved' or 'rejected'. The tool message itself is not emitted as a
 * separate n8n message.
 *
 * If a tool-result references a toolCallId not in the index (orphan), it is
 * silently dropped.

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Inspect the offending message object (log `msg` before the switch) to see the actual `role` value.
  2. If the role is valid in a newer schema version, add a `case` for it in `toAiMessageList` or add a normalization step that maps it to a known role.
  3. If the message is corrupt, filter it out during load or write a migration to fix/strip invalid roles.
  4. Ensure your message-persistence layer only writes roles that `toAiMessageList` handles.

Example fix

// before: message with role 'developer' reaches toAiMessageList → throws
// Add a case or normalize before conversion:
function toAiMessageList(msg: Message): ModelMessage[] {
  switch (msg.role) {
    // ... existing cases ...
    case 'developer':
      return [{ role: 'system', content: msg.content.filter(isText).map(b => b.text).join('') }];
    default:
      throw new Error(`Unknown role: ${msg.role as string}`);
  }
}
Defensive patterns

Strategy: type-guard

Validate before calling

const KNOWN_ROLES = new Set(['system', 'user', 'assistant', 'tool']);

function filterKnownRoles(messages: Message[]): Message[] {
  return messages.filter((m) => KNOWN_ROLES.has(m.role));
}

// Before calling toAiMessages:
const safeMessages = filterKnownRoles(allMessages);
const modelMessages = toAiMessages(safeMessages);

Type guard

function isKnownRole(msg: Message): msg is Message & { role: 'system' | 'user' | 'assistant' | 'tool' } {
  return ['system', 'user', 'assistant', 'tool'].includes(msg.role);
}

Try / catch

try {
  const modelMessages = toAiMessages(messages);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Unknown role:')) {
    // Find and quarantine the bad message, then retry
    const badRole = messages.find((m) => !['system','user','assistant','tool'].includes(m.role));
    logger.error('Corrupt message role detected', { role: badRole?.role });
  }
  throw e;
}

Prevention

When it happens

Trigger: A message row in the DB/memory store has `role: 'developer'`, `role: 'function'`, or any role not in the four handled cases. This can happen when loading messages written by a newer version of the schema, or when a custom message-serialization path stored an unsupported role.

Common situations: Upgrading the agents package to a version that changed the role enum without a migration. A deserialization bug that mangled the role field. Messages imported from an external system that uses different role names.

Related errors


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