{"record":{"id":"768d9522331c7264","repo":"n8n-io/n8n","slug":"unknown-role-msg-role-as-string","errorCode":null,"errorMessage":"Unknown role: ${msg.role as string}","messagePattern":"Unknown role: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/@n8n/agents/src/runtime/model/messages.ts","lineNumber":445,"sourceCode":"\t\t\t\tconst assistantMsg: ModelMessage = msg.providerOptions\n\t\t\t\t\t? { ...assistantBase, providerOptions: msg.providerOptions }\n\t\t\t\t\t: assistantBase;\n\t\t\t\ttransformedMessages.push(assistantMsg);\n\t\t\t}\n\t\t\tif (resultMessages.length > 0) {\n\t\t\t\ttransformedMessages.push(...resultMessages);\n\t\t\t}\n\n\t\t\treturn transformedMessages;\n\t\t}\n\n\t\tcase 'tool': {\n\t\t\t// Legacy role: 'tool' messages (from old DB rows). Don't emit them.\n\t\t\treturn [];\n\t\t}\n\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown role: ${msg.role as string}`);\n\t}\n}\n\n/** Convert n8n Messages to AI SDK ModelMessages for passing to stream/generateText. */\nexport function toAiMessages(messages: Message[]): ModelMessage[] {\n\treturn messages.flatMap(toAiMessageList);\n}\n\n/**\n * Convert AI SDK ModelMessages to n8n AgentMessages.\n *\n * This is a stateful walk: when a role:'tool' ModelMessage is encountered,\n * the matching tool-call block on the preceding assistant message is mutated\n * to 'resolved' or 'rejected'. The tool message itself is not emitted as a\n * separate n8n message.\n *\n * If a tool-result references a toolCallId not in the index (orphan), it is\n * silently dropped.","sourceCodeStart":427,"sourceCodeEnd":463,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/agents/src/runtime/model/messages.ts#L427-L463","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: message with role 'developer' reaches toAiMessageList → throws\n// Add a case or normalize before conversion:\nfunction toAiMessageList(msg: Message): ModelMessage[] {\n  switch (msg.role) {\n    // ... existing cases ...\n    case 'developer':\n      return [{ role: 'system', content: msg.content.filter(isText).map(b => b.text).join('') }];\n    default:\n      throw new Error(`Unknown role: ${msg.role as string}`);\n  }\n}","handlingStrategy":"type-guard","validationCode":"const KNOWN_ROLES = new Set(['system', 'user', 'assistant', 'tool']);\n\nfunction filterKnownRoles(messages: Message[]): Message[] {\n  return messages.filter((m) => KNOWN_ROLES.has(m.role));\n}\n\n// Before calling toAiMessages:\nconst safeMessages = filterKnownRoles(allMessages);\nconst modelMessages = toAiMessages(safeMessages);","typeGuard":"function isKnownRole(msg: Message): msg is Message & { role: 'system' | 'user' | 'assistant' | 'tool' } {\n  return ['system', 'user', 'assistant', 'tool'].includes(msg.role);\n}","tryCatchPattern":"try {\n  const modelMessages = toAiMessages(messages);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Unknown role:')) {\n    // Find and quarantine the bad message, then retry\n    const badRole = messages.find((m) => !['system','user','assistant','tool'].includes(m.role));\n    logger.error('Corrupt message role detected', { role: badRole?.role });\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["message-normalization","data-integrity","ai-sdk","role-validation"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}