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
- Pass the parsed object: payload: { text: 'hello' }, not payload: '"{...}"' or payload: '{"text":...}'
- If your transport hands you a string, JSON.parse it before the call and send the object
- 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
- Never JSON.stringify the payload — pass the object
- Wrap arrays as { items: [...] } to honor the object contract
- Parse strings arriving from transports before dispatch
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
- msgType is required
- msgType must be alnum + _ - and ≤64 chars
- agent must be an object
- basePath contains disallowed characters
- bench must be an object
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.signatureView on GitHub (pinned to 9c61c86f06)