feat(integrations): add Zapier integration for Mem0

Zapier Platform CLI app (zapier-mem0) with Add Memory / Delete Memory
creates and Search Memories / Get Memories searches against the Mem0 v3
API. Custom API-key auth (Authorization: Token), base-URL + auth
injected via beforeRequest middleware, non-2xx surfaced as errors.

Add Memory polls GET /v1/event/{id}/ for async extraction. Includes an
E2E test suite via createAppTester.
This commit is contained in:
Himanshu-Sangshetti
2026-07-20 11:55:29 +05:30
parent 4464dca825
commit 9882eea7b8
12 changed files with 4931 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
node_modules/
.env
.zapierapprc
build/
coverage/
*.log
.DS_Store
+33
View File
@@ -0,0 +1,33 @@
# Zapier integration for Mem0
A [Zapier](https://zapier.com) integration for [Mem0](https://mem0.ai) — the memory layer for AI agents. Add, search, list, and delete long-term memories from any Zap.
Built with the [Zapier Platform CLI](https://docs.zapier.com/platform/quickstart/cli-tutorial).
## Actions
| Type | Name | Endpoint |
| --- | --- | --- |
| Create | **Add Memory** | `POST /v3/memories/add/` |
| Create | **Delete Memory** | `DELETE /v1/memories/{id}/` |
| Search | **Search Memories** | `POST /v3/memories/search/` |
| Search | **Get Memories** | `POST /v3/memories/` |
**Add Memory** runs LLM extraction asynchronously by default; the action polls the event until it completes and returns the resulting memories. Turn off **Wait for Completion** to return immediately, or set **Infer = false** to store verbatim (synchronous).
## Authentication
Custom (API key) auth. Provide a Mem0 API key from [app.mem0.ai](https://app.mem0.ai) → Settings → API Keys. It is sent as `Authorization: Token <key>`.
## Development
```bash
npm install
MEM0_API_KEY=m0-... npm test # runs the E2E suite against api.mem0.ai
```
To deploy (maintainers): `zapier login && zapier push`.
## License
MIT
@@ -0,0 +1,31 @@
'use strict';
// Custom (API key) authentication for Mem0.
// The key is sent as "Authorization: Token <apiKey>" (matches Mem0's SDKs).
const test = (z, _bundle) =>
z.request({ url: '/v1/ping/', method: 'GET' });
module.exports = {
type: 'custom',
test,
fields: [
{
key: 'apiKey',
label: 'Mem0 API Key',
type: 'string',
required: true,
helpText:
'Your Mem0 API key (starts with `m0-`). Create one at [app.mem0.ai](https://app.mem0.ai) → Settings → API Keys.',
},
{
key: 'baseUrl',
label: 'Base URL',
type: 'string',
required: false,
default: 'https://api.mem0.ai',
helpText: 'Override only for self-hosted or non-default deployments.',
},
],
// Shown on the connection label in the Zap editor.
connectionLabel: 'Mem0 ({{bundle.authData.baseUrl}})',
};
@@ -0,0 +1,94 @@
'use strict';
const POLL_INTERVAL_MS = 1500;
const MAX_POLL_ATTEMPTS = 40; // ~60s
// Polls GET /v1/event/{id}/ until the async memory-addition event resolves.
const pollEvent = async (z, eventId) => {
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
const res = await z.request({ url: `/v1/event/${eventId}/`, method: 'GET' });
const status = res.data && res.data.status;
if (status === 'SUCCEEDED' || status === 'FAILED') {
return res.data;
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new Error(`Timed out waiting for memory event ${eventId} to complete`);
};
const perform = async (z, bundle) => {
const body = {
messages: [{ role: bundle.inputData.role || 'user', content: bundle.inputData.content }],
infer: bundle.inputData.infer !== undefined ? bundle.inputData.infer : true,
};
if (bundle.inputData.user_id) body.user_id = bundle.inputData.user_id;
if (bundle.inputData.agent_id) body.agent_id = bundle.inputData.agent_id;
if (bundle.inputData.run_id) body.run_id = bundle.inputData.run_id;
if (bundle.inputData.metadata) {
body.metadata =
typeof bundle.inputData.metadata === 'string'
? JSON.parse(bundle.inputData.metadata)
: bundle.inputData.metadata;
}
const response = await z.request({
url: '/v3/memories/add/',
method: 'POST',
body,
});
const data = response.data;
const wait = bundle.inputData.waitForCompletion !== false;
// infer=true returns {event_id, status:PENDING}; optionally wait for the result.
if (wait && data.status && data.status !== 'SUCCEEDED' && data.event_id) {
return pollEvent(z, data.event_id);
}
return data;
};
module.exports = {
key: 'add_memory',
noun: 'Memory',
display: {
label: 'Add Memory',
description: 'Extract and store memories from a message.',
},
operation: {
perform,
inputFields: [
{
key: 'content',
label: 'Content',
type: 'text',
required: true,
helpText: 'The message content to extract memories from.',
},
{
key: 'role',
label: 'Role',
choices: { user: 'User', assistant: 'Assistant', system: 'System' },
default: 'user',
},
{ key: 'user_id', label: 'User ID', type: 'string' },
{ key: 'agent_id', label: 'Agent ID', type: 'string' },
{ key: 'run_id', label: 'Run ID', type: 'string' },
{ key: 'metadata', label: 'Metadata (JSON)', type: 'string' },
{
key: 'infer',
label: 'Infer',
type: 'boolean',
default: 'true',
helpText: 'Run LLM extraction (async). Turn off to store verbatim (synchronous).',
},
{
key: 'waitForCompletion',
label: 'Wait for Completion',
type: 'boolean',
default: 'true',
helpText: 'Poll until extraction finishes and return the resulting memories.',
},
],
sample: { status: 'SUCCEEDED', event_id: '00000000-0000-0000-0000-000000000000', results: [] },
},
};
@@ -0,0 +1,26 @@
'use strict';
const perform = async (z, bundle) => {
// Trailing slash required (Django APPEND_SLASH).
const response = await z.request({
url: `/v1/memories/${bundle.inputData.memory_id}/`,
method: 'DELETE',
});
return response.data || { message: 'Deleted', memory_id: bundle.inputData.memory_id };
};
module.exports = {
key: 'delete_memory',
noun: 'Memory',
display: {
label: 'Delete Memory',
description: 'Delete a single memory by its ID.',
},
operation: {
perform,
inputFields: [
{ key: 'memory_id', label: 'Memory ID', type: 'string', required: true },
],
sample: { message: 'Memory deleted successfully' },
},
};
+35
View File
@@ -0,0 +1,35 @@
'use strict';
const authentication = require('./authentication');
const { includeApiKey, handleBadResponses } = require('./middleware');
const addMemory = require('./creates/add_memory');
const deleteMemory = require('./creates/delete_memory');
const searchMemories = require('./searches/search_memories');
const getMemories = require('./searches/get_memories');
const { version } = require('./package.json');
const platformVersion = require('zapier-platform-core').version;
module.exports = {
version,
platformVersion,
authentication,
beforeRequest: [includeApiKey],
afterResponse: [handleBadResponses],
creates: {
[addMemory.key]: addMemory,
[deleteMemory.key]: deleteMemory,
},
searches: {
[searchMemories.key]: searchMemories,
[getMemories.key]: getMemories,
},
resources: {},
triggers: {},
};
+29
View File
@@ -0,0 +1,29 @@
'use strict';
// Prepend the configured base URL and inject the auth header on every request.
const includeApiKey = (request, z, bundle) => {
if (bundle.authData && bundle.authData.apiKey) {
request.headers = request.headers || {};
request.headers.Authorization = `Token ${bundle.authData.apiKey}`;
}
// Resolve relative URLs against the configured base URL.
if (request.url && request.url.startsWith('/')) {
const base = (bundle.authData && bundle.authData.baseUrl) || 'https://api.mem0.ai';
request.url = `${base.replace(/\/$/, '')}${request.url}`;
}
return request;
};
// Surface auth failures as Zapier auth errors so users are prompted to reconnect.
const handleBadResponses = (response, z, _bundle) => {
if (response.status === 401 || response.status === 403) {
throw new z.errors.Error(
'The Mem0 API key you supplied is invalid or lacks access.',
'AuthenticationError',
response.status,
);
}
return response;
};
module.exports = { includeApiKey, handleBadResponses };
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "zapier-mem0",
"version": "0.1.0",
"description": "Zapier integration for Mem0 — the memory layer for AI agents.",
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "integrations/zapier-mem0"
},
"license": "MIT",
"main": "index.js",
"scripts": {
"test": "jest --testTimeout 120000"
},
"engines": {
"node": ">=18",
"npm": ">=5.6.0"
},
"dependencies": {
"zapier-platform-core": "19.0.0"
},
"devDependencies": {
"jest": "^29.7.0"
},
"private": true
}
@@ -0,0 +1,32 @@
'use strict';
const perform = async (z, bundle) => {
const body = {};
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
const response = await z.request({
url: '/v3/memories/',
method: 'POST',
params: { page: 1, page_size: bundle.inputData.limit || 50 },
body,
});
const data = response.data;
return Array.isArray(data) ? data : data.results || [];
};
module.exports = {
key: 'get_memories',
noun: 'Memory',
display: {
label: 'Get Memories',
description: 'List stored memories for a user.',
},
operation: {
perform,
inputFields: [
{ key: 'user_id', label: 'User ID', type: 'string' },
{ key: 'limit', label: 'Limit', type: 'integer', default: '50' },
],
sample: { id: '00000000-0000-0000-0000-000000000000', memory: 'User loves hiking' },
},
};
@@ -0,0 +1,37 @@
'use strict';
const perform = async (z, bundle) => {
const body = {
query: bundle.inputData.query,
output_format: 'v1.1',
limit: bundle.inputData.limit || 10,
};
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
const response = await z.request({
url: '/v3/memories/search/',
method: 'POST',
body,
});
// Searches must return an array.
const data = response.data;
return Array.isArray(data) ? data : data.results || [];
};
module.exports = {
key: 'search_memories',
noun: 'Memory',
display: {
label: 'Search Memories',
description: 'Semantic search over stored memories.',
},
operation: {
perform,
inputFields: [
{ key: 'query', label: 'Query', type: 'string', required: true },
{ key: 'user_id', label: 'User ID', type: 'string' },
{ key: 'limit', label: 'Limit', type: 'integer', default: '10' },
],
sample: { id: '00000000-0000-0000-0000-000000000000', memory: 'User loves hiking' },
},
};
@@ -0,0 +1,69 @@
'use strict';
/* global describe, it, expect, beforeAll */
const zapier = require('zapier-platform-core');
const App = require('../index');
const appTester = zapier.createAppTester(App);
const authData = {
apiKey: process.env.MEM0_API_KEY,
baseUrl: process.env.MEM0_BASE_URL || 'https://api.mem0.ai',
};
const userId = `zapier-e2e-${Date.now()}`;
describe('Mem0 Zapier integration (E2E)', () => {
beforeAll(() => {
if (!authData.apiKey) throw new Error('MEM0_API_KEY env var is required for E2E tests');
});
it('authentication.test succeeds', async () => {
const res = await appTester(App.authentication.test, { authData });
expect(res.status).toBe(200);
});
it('adds, searches, lists, and deletes a memory', async () => {
// Add (async, wait for completion)
const added = await appTester(App.creates.add_memory.operation.perform, {
authData,
inputData: {
content: 'I love hiking in the Alps and my favorite food is sushi',
user_id: userId,
waitForCompletion: true,
},
});
expect(added.status).toBe('SUCCEEDED');
// Search
const found = await appTester(App.searches.search_memories.operation.perform, {
authData,
inputData: { query: 'outdoor activities', user_id: userId, limit: 5 },
});
expect(Array.isArray(found)).toBe(true);
expect(found.length).toBeGreaterThan(0);
// Get all
const all = await appTester(App.searches.get_memories.operation.perform, {
authData,
inputData: { user_id: userId },
});
expect(Array.isArray(all)).toBe(true);
expect(all.length).toBeGreaterThan(0);
// Cleanup: delete every memory we created
for (const mem of all) {
await appTester(App.creates.delete_memory.operation.perform, {
authData,
inputData: { memory_id: mem.id },
});
}
const afterDelete = await appTester(App.searches.get_memories.operation.perform, {
authData,
inputData: { user_id: userId },
});
expect(afterDelete.length).toBe(0);
});
});