ruvnet/ruflo · error

payload must be a JSON object

Error message

payload must be a JSON object

What it means

The agentbbs post handler requires payload to be a JSON object (typeof 'object' and not null) because it becomes the BbsEnvelope's structured body that gets sequenced and persisted. Strings, numbers, booleans, and null throw; note that a JSON.stringify'd string is the most common offender — double-encoded payloads are rejected.

Solutions

  1. Pass the parsed object: payload: { text: 'hello' }, not payload: '"{...}"' or payload: '{"text":...}'
  2. If your transport hands you a string, JSON.parse it before the call and send the object
  3. Send arrays wrapped in an object ({ items: [...] }) to honor the object contract even though arrays slip past the check

Example fix

// before — double-encoded payload
await callMCPTool('agentbbs_post', { roomId, msgType: 'chat_message', payload: JSON.stringify({ text: 'hi' }) });

// after — plain object
await callMCPTool('agentbbs_post', { roomId, msgType: 'chat_message', payload: { text: 'hi' } });
Defensive patterns

Strategy: validation

Validate before calling

function toPayload(v: unknown): Record<string, unknown> {
  if (typeof v === 'string') {
    try { v = JSON.parse(v); } catch { throw new Error('payload is a non-JSON string — pass the object'); }
  }
  if (typeof v !== 'object' || v === null || Array.isArray(v)) {
    throw new Error('payload must be a JSON object');
  }
  return v as Record<string, unknown>;
}

Type guard

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

Prevention

When it happens

Trigger: Passing payload: JSON.stringify(data) instead of the object itself; payload: null or omitted (it is schema-required); payload as a bare string/number; arrays technically pass the typeof check but violate the object intent.

Common situations: Transport layers that serialize nested objects before dispatch (HTTP query strings, some IPC bridges); defensive 'stringify early' habits; copy-pasting curl examples where JSON was quoted.

Related errors


AI-assisted analysis of ruvnet/ruflo@9c61c86f06 (2026-09-15). Data as JSON: /api/errors/15c8088fa2313e28. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/cli/src/mcp-tools/agentbbs-tools.ts:403

          description: 'Event-specific JSON-serializable payload.',
        },
        signature: {
          type: 'string',
          description: 'Optional Ed25519 signature over the canonical envelope bytes. Phase 1: pass-through.',
        },
      },
      required: ['roomId', 'msgType', 'payload'],
    },
    handler: async (input) => {
      const basePath = resolveBasePath(input.basePath as string | undefined);
      const roomId = validateRoomId(String(input.roomId));
      const msgType = String(input.msgType ?? '');
      if (!msgType) throw new Error('msgType is required');
      if (msgType.length > 64 || !/^[A-Za-z0-9_-]+$/.test(msgType)) {
        throw new Error('msgType must be alnum + _ - and ≤64 chars');
      }
      if (typeof input.payload !== 'object' || input.payload === null) {
        throw new Error('payload must be a JSON object');
      }

      if (!agentbbsCliAvailable()) return degradedResult('agentbbs-not-found');

      ensureDir(basePath);
      const logPath = roomLogPath(basePath, roomId);
      const base: BbsEnvelope = {
        envelopeId: base64url(randomBytes(12)),
        roomId,
        seq: nextSeq(logPath),
        msgType,
        payload: input.payload,
        timestamp: new Date().toISOString(),
      };
      // Phase 2: sign with this host's persistent node identity so peers can
      // attribute and verify the envelope after a cross-host merge. An
      // explicitly supplied signature is preserved rather than overwritten.
      const env: BbsEnvelope = input.signature

View on GitHub (pinned to 9c61c86f06)