mem0ai/mem0 · warning · NodeOperationError
Mem0 memory add failed: ${(addResp.message as string) || 'un
Error message
Mem0 memory add failed: ${(addResp.message as string) || 'unknown error'} What it means
GET /memories with no user_id/run_id/agent_id means 'list ALL memories in the store', which the server treats as an admin-only operation. If the caller is authenticated but their role is not admin (and auth is not via ADMIN_API_KEY/AUTH_DISABLED), the request is rejected with 403. Providing any single identifier scopes the query and avoids the check entirely.
Source
Thrown at integrations/n8n-nodes-mem0/nodes/Mem0/Mem0.node.ts:472
// API requires at least one entity id — fail clearly instead of a raw 4xx.
if (!body.user_id && !body.agent_id && !body.run_id && !body.app_id) {
throw new NodeOperationError(
this.getNode(),
'Add requires at least one of User ID, Agent ID, Run ID, or App ID',
{ itemIndex: i },
);
}
const addResp = await request('POST', '/v3/memories/add/', body);
const waitForCompletion = this.getNodeParameter('waitForCompletion', i, true) as boolean;
const addStatus = addResp.status as string | undefined;
const isTerminal = addStatus === 'SUCCEEDED' || addStatus === 'FAILED';
// Add returns {event_id, status:PENDING|RUNNING}; poll until terminal when asked to wait.
if (waitForCompletion && addResp.event_id && !isTerminal) {
responseData = await pollEvent(request, addResp.event_id as string, this, i);
} else if (addStatus === 'FAILED') {
throw new NodeOperationError(
this.getNode(),
`Mem0 memory add failed: ${(addResp.message as string) || 'unknown error'}`,
{ itemIndex: i },
);
} else {
// If the response is already terminal, unwrap results; otherwise return as-is.
responseData = Array.isArray(addResp.results)
? (addResp.results as IDataObject[])
: addResp;
}
} else if (operation === 'search') {
const body: IDataObject = {
query: this.getNodeParameter('query', i) as string,
output_format: 'v1.1',
top_k: this.getNodeParameter('limit', i, 50) as number,
};
body.filters = buildEntityFilters(
{View on GitHub (pinned to 001c235229)
Solutions
- Add at least one scope query parameter: GET /memories?user_id=alice (or agent_id/run_id).
- Or authenticate as an admin-role user / use ADMIN_API_KEY for the unscoped listing.
- Fix the client to always derive and pass the current user's identifier instead of issuing unscoped GETs.
Example fix
# before
requests.get(f"{BASE}/memories", headers=headers) # 403 for member user
# after
requests.get(f"{BASE}/memories", params={"user_id": "alice"}, headers=headers) Defensive patterns
Strategy: validation
Validate before calling
def list_memories(base: str, headers: dict, user_id: str | None) -> dict:
params = {"user_id": user_id} if user_id else None
if params is None:
assert headers.get("Authorization") and is_admin(headers), \
"Unscoped GET /memories requires admin credentials"
return requests.get(f"{base}/memories", params=params, headers=headers).json() Type guard
def is_admin(headers: dict) -> bool:
# decode own JWT payload (middle segment) and inspect role claim
import base64, json
tok = headers.get("Authorization", "").removeprefix("Bearer ")
if not tok:
return False
payload = tok.split(".")[1]
claims = json.loads(base64.urlsafe_b64decode(payload + "=" * (-len(payload) % 4)))
return claims.get("role") == "admin" Try / catch
if resp.status_code == 403 and "Admin role required" in resp.text:
raise PermissionError("Scope the request (user_id/agent_id/run_id) or use admin credentials") Prevention
- Default clients to always pass a scope parameter.
- Reserve unscoped listing for admin tooling.
- Teach dashboards to page through scoped queries per actor.
When it happens
Trigger: A member-role user or their personal API key calling GET /memories with no query params; a client that 'lists everything' on startup by omitting filters; admin-key auth not actually matching ADMIN_API_KEY so the caller fell into the API-key user path.
Common situations: Dashboard-style clients enumerating all memories for a demo; multi-user deployments where non-admin users share one memory store; scripts written under AUTH_DISABLED that later run against an authenticated server.
Related errors
- HTTP ${resp.status}: ${detail}
- At least one entity ID is required for deleteEntities.
- Add requires at least one of User ID, Agent ID, Run ID, or A
- Timed out waiting for memory event ${eventId} to complete
- Authentication failed. Your API key may be invalid or expire
AI-assisted analysis of mem0ai/mem0@001c235229 (2026-08-15).
Data as JSON: /api/errors/0aea116e058a79ee.
Report an issue: GitHub.