ruvnet/ruflo · error

Event must have a valid aggregateId string

Error message

Event must have a valid aggregateId string

What it means

RvfEventLog.append() validates the incoming DomainEvent before persisting and requires aggregateId to be a non-empty string, because it keys per-aggregate versioning (the aggregateVersions map) and the on-disk index on that field. Events missing, blanking, or carrying a non-string aggregateId are rejected before any write, keeping the log consistent. The usual culprit is an event assembled from spreads or deserialized JSON where the field was dropped or renamed.

Source

Thrown at v3/@claude-flow/shared/src/events/rvf-event-log.ts:163

    this.events = [];
    this.aggregateIndex.clear();
    this.aggregateVersions.clear();
    this.snapshots.clear();
    this.initialized = false;

    this.emit('shutdown');
  }

  // ===========================================================================
  // Write Operations
  // ===========================================================================

  /** Append a domain event to the log. */
  async append(event: DomainEvent): Promise<void> {
    this.ensureInitialized();

    if (!event.aggregateId || typeof event.aggregateId !== 'string') {
      throw new Error('Event must have a valid aggregateId string');
    }
    if (!event.type || typeof event.type !== 'string') {
      throw new Error('Event must have a valid type string');
    }

    // Assign next version for aggregate
    const currentVersion = this.aggregateVersions.get(event.aggregateId) ?? 0;
    const nextVersion = currentVersion + 1;
    event.version = nextVersion;

    // Persist to disk first (crash-safe ordering)
    this.appendRecord(this.config.logPath, event);

    // Update in-memory state
    this.indexEvent(event);

    this.emit('event:appended', event);

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Set aggregateId explicitly at every append site — String(aggregate.id) if the id is numeric
  2. Fix serialization so the field arrives named exactly aggregateId
  3. Centralize event construction in a helper that validates the DomainEvent shape once

Example fix

// before
await log.append({ type: 'order.updated', payload } as DomainEvent); // throws

// after
await log.append({ aggregateId: String(order.id), type: 'order.updated', payload } as DomainEvent);
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof event.aggregateId !== 'string' || event.aggregateId.length === 0) {
  throw new TypeError('DomainEvent.aggregateId must be a non-empty string');
}
await log.append(event);

Type guard

function isDomainEvent(e: unknown): e is DomainEvent {
  return !!e && typeof e === 'object'
    && typeof (e as any).aggregateId === 'string' && (e as any).aggregateId.length > 0
    && typeof (e as any).type === 'string' && (e as any).type.length > 0;
}

Try / catch

try {
  await log.append(event);
} catch (e) {
  if (e instanceof Error && e.message.includes('aggregateId')) {
    // fix the producer: the event was missing/blank/non-string aggregateId
  } else throw e;
}

Prevention

When it happens

Trigger: append({ type: 'task.created', payload }) with no aggregateId; aggregateId: '' or undefined; aggregateId supplied as a number (e.g. 42) or any non-string value.

Common situations: Events built via { ...base, type } that accidentally drop the id; JSON deserialized with a field-name mismatch (aggregate_id vs aggregateId); test fixtures missing the field; ORM rows used directly as events.

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 ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/f6222c8e422d3b89. Report an issue: GitHub.