fix(n8n): restore App ID, Includes and Excludes, add App ID filters

Reverts the removal in 2a61fe1a. That commit dropped app_id, includes and
excludes from Add because docs/openapi.json does not declare them on
POST /v3/memories/add/. The schema is incomplete: probing the live endpoint
shows all three are accepted and honoured.

- app_id is persisted and is queryable via /v3/memories/search/ and
  /v3/memories/, so the entity-id guard accepts it again as a valid scope
- includes/excludes demonstrably steer extraction. The same message stored
  three memories unfiltered and one under
  includes: "only record food and diet preferences"

Search and Get Many gain an App ID filter alongside User/Agent/Run. Writing
an app-scoped memory was otherwise unreadable from this node, and app_id is
how the OpenCode and Pi Agent plugins express project scope.

buildEntityFilters now takes the ids as an object rather than four adjacent
positional strings, so a fifth entity is a one-key change.
This commit is contained in:
kartik-mem0
2026-07-29 22:18:19 +05:30
parent a8e2a49209
commit 29601e54f4
4 changed files with 120 additions and 26 deletions
@@ -163,6 +163,12 @@ export class Mem0 implements INodeType {
type: 'string',
default: '',
},
{
displayName: 'App ID',
name: 'app_id',
type: 'string',
default: '',
},
{
displayName: 'Custom Categories',
name: 'custom_categories',
@@ -180,6 +186,20 @@ export class Mem0 implements INodeType {
description:
'Optional instructions that steer what the extractor keeps or ignores',
},
{
displayName: 'Excludes',
name: 'excludes',
type: 'string',
default: '',
description: 'Optional: skip memories matching this description',
},
{
displayName: 'Includes',
name: 'includes',
type: 'string',
default: '',
description: 'Optional: only extract memories matching this description',
},
{
displayName: 'Infer',
name: 'infer',
@@ -221,7 +241,7 @@ export class Mem0 implements INodeType {
default: '',
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
description:
'Restrict the search to this user. Supply at least one of User ID, Agent ID, or Run ID.',
'Restrict the search to this user. Supply at least one of User ID, Agent ID, App ID, or Run ID.',
},
{
displayName: 'Agent ID',
@@ -231,6 +251,14 @@ export class Mem0 implements INodeType {
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
description: 'Restrict the search to memories scoped to this agent',
},
{
displayName: 'App ID',
name: 'appId',
type: 'string',
default: '',
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
description: 'Restrict the search to memories scoped to this app or project',
},
{
displayName: 'Run ID',
name: 'runId',
@@ -257,7 +285,7 @@ export class Mem0 implements INodeType {
default: '',
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
description:
'Restrict the listing to this user. Supply at least one of User ID, Agent ID, or Run ID.',
'Restrict the listing to this user. Supply at least one of User ID, Agent ID, App ID, or Run ID.',
},
{
displayName: 'Agent ID',
@@ -267,6 +295,14 @@ export class Mem0 implements INodeType {
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
description: 'Restrict the listing to memories scoped to this agent',
},
{
displayName: 'App ID',
name: 'appId',
type: 'string',
default: '',
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
description: 'Restrict the listing to memories scoped to this app or project',
},
{
displayName: 'Run ID',
name: 'runId',
@@ -378,6 +414,7 @@ export class Mem0 implements INodeType {
const userId = this.getNodeParameter('userId', i, '') as string;
if (userId) body.user_id = userId;
if (addFields.agent_id) body.agent_id = addFields.agent_id;
if (addFields.app_id) body.app_id = addFields.app_id;
if (addFields.run_id) body.run_id = addFields.run_id;
if (addFields.metadata) {
try {
@@ -411,11 +448,14 @@ export class Mem0 implements INodeType {
}
}
if (addFields.includes) body.includes = addFields.includes;
if (addFields.excludes) body.excludes = addFields.excludes;
// API requires at least one entity id — fail clearly instead of a raw 4xx.
if (!body.user_id && !body.agent_id && !body.run_id) {
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, or Run ID',
'Add requires at least one of User ID, Agent ID, Run ID, or App ID',
{ itemIndex: i },
);
}
@@ -447,9 +487,12 @@ export class Mem0 implements INodeType {
top_k: this.getNodeParameter('limit', i, 50) as number,
};
body.filters = buildEntityFilters(
this.getNodeParameter('userId', i, '') as string,
this.getNodeParameter('agentId', i, '') as string,
this.getNodeParameter('runId', i, '') as string,
{
user_id: this.getNodeParameter('userId', i, '') as string,
agent_id: this.getNodeParameter('agentId', i, '') as string,
app_id: this.getNodeParameter('appId', i, '') as string,
run_id: this.getNodeParameter('runId', i, '') as string,
},
this,
i,
);
@@ -460,9 +503,12 @@ export class Mem0 implements INodeType {
const pageSize = this.getNodeParameter('pageSize', i, 50) as number;
const body: IDataObject = {
filters: buildEntityFilters(
this.getNodeParameter('userId', i, '') as string,
this.getNodeParameter('agentId', i, '') as string,
this.getNodeParameter('runId', i, '') as string,
{
user_id: this.getNodeParameter('userId', i, '') as string,
agent_id: this.getNodeParameter('agentId', i, '') as string,
app_id: this.getNodeParameter('appId', i, '') as string,
run_id: this.getNodeParameter('runId', i, '') as string,
},
this,
i,
),
@@ -529,21 +575,18 @@ export class Mem0 implements INodeType {
}
function buildEntityFilters(
userId: string,
agentId: string,
runId: string,
ids: Record<string, string>,
ctx: IExecuteFunctions,
itemIndex: number,
): IDataObject {
const clauses: IDataObject[] = [];
if (userId) clauses.push({ user_id: userId });
if (agentId) clauses.push({ agent_id: agentId });
if (runId) clauses.push({ run_id: runId });
const clauses: IDataObject[] = Object.entries(ids)
.filter(([, value]) => value)
.map(([key, value]) => ({ [key]: value }));
if (clauses.length === 0) {
throw new NodeOperationError(
ctx.getNode(),
'Provide at least one of User ID, Agent ID, or Run ID',
'Provide at least one of User ID, Agent ID, App ID, or Run ID',
{ itemIndex },
);
}