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
- Inspect the offending message object (log `msg` before the switch) to see the actual `role` value.
- 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.
- If the message is corrupt, filter it out during load or write a migration to fix/strip invalid roles.
- 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
- Validate message roles at the persistence boundary — reject writes with unknown roles.
- Run a migration to normalize or strip legacy roles when upgrading the agents package.
- Add a defensive filter before `toAiMessages` to drop unknown-role messages with a warning.
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
- Delegated child checkpoint metadata is missing or invalid
- Failed to parse ${label} at ${path}: ${msg}
- Existing manifest at ${manifestPath} is malformed; remove or
- bucketFromEvaluation: no fileSlug for test case "${caseDispl
- Found multiple eval-results entries for test case slug "${sl
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/768d9522331c7264.
Report an issue: GitHub.