mastra-ai/mastra · error · FactoryRuleValidationError

Rule metadata must be an object.

Error message

Rule metadata must be an object.

What it means

The metadata field of an upsertLinkedWorkItem decision must normalize to a plain object of JSON values (or be omitted). If the top-level metadata value is an array, string, number, or boolean, sanitizeMetadata throws because downstream consumers expect a Record<string, FactoryRuleJsonValue>. Additionally the serialized metadata must stay under 16,384 characters ('Rule metadata is too large').

Source

Thrown at mastracode/factory/src/rules/validation.ts:135

      throw new FactoryRuleValidationError('Rule metadata contains too many fields.');
    }
    const sanitized: Record<string, FactoryRuleJsonValue> = {};
    for (const [key, entry] of entries) {
      const normalizedKey = boundedString(key, 'Rule metadata key', 128, IDENTIFIER_RE);
      sanitized[normalizedKey] = SENSITIVE_KEY_RE.test(normalizedKey)
        ? '[REDACTED]'
        : normalizeFactoryRuleJsonValue(entry, depth + 1, seen);
    }
    return sanitized;
  } finally {
    seen.delete(value as object);
  }
}

function sanitizeMetadata(value: unknown): Record<string, FactoryRuleJsonValue> | undefined {
  if (value === undefined) return undefined;
  const sanitized = normalizeFactoryRuleJsonValue(value);
  if (!isPlainObject(sanitized)) throw new FactoryRuleValidationError('Rule metadata must be an object.');
  if (JSON.stringify(sanitized).length > MAX_METADATA_JSON_LENGTH) {
    throw new FactoryRuleValidationError('Rule metadata is too large.');
  }
  return sanitized;
}

function validateBoardRules(rules: unknown, label: string): asserts rules is FactoryBoardRules {
  if (!isPlainObject(rules)) throw new FactoryRuleValidationError(`${label} must be an object.`);
  for (const [stage, sources] of Object.entries(rules)) {
    enumValue(stage, FACTORY_RULE_STAGES, `${label} stage`);
    if (!isPlainObject(sources)) throw new FactoryRuleValidationError(`${label}.${stage} must be an object.`);
    for (const [source, leaf] of Object.entries(sources)) {
      enumValue(source, FACTORY_RULE_SOURCES, `${label}.${stage} source`);
      if (!isPlainObject(leaf)) throw new FactoryRuleValidationError(`${label}.${stage}.${source} must be an object.`);
      assertExactKeys(leaf, ['onEnter', 'onExit'], `${label}.${stage}.${source}`);
      for (const handler of Object.values(leaf)) {
        if (handler !== undefined && typeof handler !== 'function') {
          throw new FactoryRuleValidationError(`${label}.${stage}.${source} handlers must be functions.`);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Wrap the value in an object: metadata: { items: [...] } instead of metadata: [...].
  2. Omit the metadata field entirely if there is nothing to attach.
  3. If JSON.stringify(value).length > 16384, summarize, truncate lists, or move bulk data out of metadata.
  4. Validate the shape (plain object) and approximate serialized size before emitting the decision.

Example fix

// before
metadata: ['build-log', 'coverage']
// after
metadata: { labels: ['build-log', 'coverage'] }
Defensive patterns

Strategy: validation

Validate before calling

const safeMetadata = (m: unknown): Record<string, unknown> | undefined => {
  if (m === undefined) return undefined;
  if (typeof m !== 'object' || m === null || Array.isArray(m)) throw new TypeError('metadata must be an object');
  if (JSON.stringify(m).length > 16384) throw new RangeError('metadata exceeds 16 KiB');
  return m as Record<string, unknown>;
};

Type guard

const isMetadataShape = (v: unknown): v is Record<string, unknown> =>
  typeof v === 'object' && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;

Try / catch

try {
  emit({ type: 'upsertLinkedWorkItem', metadata, /* ... */ });
} catch (e) {
  if (e instanceof FactoryRuleValidationError && /metadata (must be an object|is too large)/.test(e.message)) {
    emit({ type: 'upsertLinkedWorkItem', metadata: { summary: JSON.stringify(metadata).slice(0, 2000) } });
  } else throw e;
}

Prevention

When it happens

Trigger: Passing metadata as an array (e.g. metadata: ['a','b']), a bare string, or a number instead of an object; or passing an object whose JSON.stringify exceeds 16 KiB after sanitization/redaction.

Common situations: Confusing metadata (key-value record) with a payload/list field, storing a whole report blob in metadata and hitting the 16 KiB cap, LLM-generated decisions putting a list in metadata.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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