mastra-ai/mastra · error

Expected role "system" but saw ${coreMessage.role} for messa

Error message

Expected role "system" but saw ${coreMessage.role} for message ${JSON.stringify(coreMessage, null, 2)}

What it means

MessageList.addSystem asserts that everything passed to it is a system message. systemMessageToAIV4Core normalized the input, and if the resulting coreMessage.role is not 'system', the call site passed a non-system message (or an unconvertible shape) to addSystem, so it throws.

Source

Thrown at packages/core/src/agent/message-list/message-list.ts:1578

      | string[]
      | null,
    tag?: string,
  ) {
    if (!messages) return this;
    for (const message of Array.isArray(messages) ? messages : [messages]) {
      this.addOneSystem(message, tag);
    }
    return this;
  }

  private addOneSystem(
    message: CoreMessageV4 | AIV6Type.ModelMessage | AIV5Type.ModelMessage | MastraDBMessage | string,
    tag?: string,
  ) {
    const coreMessage = systemMessageToAIV4Core(message);

    if (coreMessage.role !== `system`) {
      throw new Error(
        `Expected role "system" but saw ${coreMessage.role} for message ${JSON.stringify(coreMessage, null, 2)}`,
      );
    }

    if (tag && !this.isDuplicateSystem(coreMessage, tag)) {
      this.taggedSystemMessages[tag] ||= [];
      this.taggedSystemMessages[tag].push(coreMessage);
      if (this.isRecording) {
        this.recordedEvents.push({
          type: 'addSystem',
          tag,
          message: coreMessage,
        });
      }
    } else if (!tag && !this.isDuplicateSystem(coreMessage)) {
      this.systemMessages.push(coreMessage);
      if (this.isRecording) {
        this.recordedEvents.push({

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Only pass messages with role 'system' (or plain strings) to addSystem
  2. Route non-system messages to messageList.add() instead
  3. Log/inspect the JSON in the error to identify the wrongly-typed input
  4. Add a compile-time constraint (SystemMessage type) in your calling code

Example fix

// before
messageList.addSystem({ role: 'user', content: 'instructions' })
// after
messageList.addSystem({ role: 'system', content: 'instructions' })
Defensive patterns

Strategy: validation

Validate before calling

if (typeof msg !== 'string' && msg.role !== 'system') throw new Error('addSystem requires a system message');

Type guard

function isSystemMessage(m: any): m is { role: 'system'; content: string | any[] } { return typeof m === 'string' || m?.role === 'system'; }

Try / catch

try { messageList.addSystem(msg); } catch (e) { if (e instanceof Error && e.message.includes('Expected role "system"')) { messageList.add(msg); } else throw e; }

Prevention

When it happens

Trigger: Calling messageList.addSystem() with a user/assistant message, a plain object missing a role, or a string-producing conversion that yields a non-system role; also passing MastraDBMessages whose role was mutated.

Common situations: Using addSystem as a generic add() by mistake; dynamic system-prompt injection passing a variable that is not always a system message; typed-erased inputs from JS callers.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/0e92a96717f95102. Report an issue: GitHub.