All articles
June 14, 202618 min read

Building an AI Agent Platform in erxes CRM in 12 Days

A case study of how I built an AI agent platform in erxes CRM in 12 days with Mastra, addressing RAG, tools, and production architecture.

aiagentsragmastrallmerxes

Why Is Something Like This Needed? (In Plain Language)

Imagine you have a small business. Customer information, sales contracts, tasks, chat conversations — everything is stored in the CRM. But to find something out, you have to click through ten or twenty windows, configure filters, and compile reports. For an ordinary employee without technical expertise, this takes a lot of time and is sometimes simply impossible.

What I built changes that: you simply open a chat and ask as if you were talking to a person.

  • "How many new customers were added this month?" — the agent counts them and answers.
  • "Please pull up the information for the customer named Bat" — the agent finds it.
  • "Create a new contract" — the agent creates it.
  • "Send me a summary of new sales every week" — the agent starts doing it automatically.

Most importantly: the agent will never show you information you are not authorized to view. If you do not have permission to access that data in the system, the agent will not show it either — just like a trusted human assistant, except it never gets tired and works at night too.

That is the "what" from an ordinary user's perspective. Now let's break down the "how" below — the technical details, step by step.

1. What erxes is and why an agent platform belongs within it

For work on agents to be meaningful, you first need to understand what they live inside — the overall architecture of that system.

This plugin architecture is the single most important fact in this entire case study. It is precisely what allowed me to add a completely new "AI Agents" product to erxes without modifying any of the existing plugins — and, as we will see, this architecture also made it possible for agents to automatically interact with every other plugin.

The goal I set was easy to state and difficult to achieve:

An ordinary, non-technical business user opens a chat and interacts with an AI agent in natural language, and that agent actually takes action within their erxes — searching for a contact, creating a deal, answering questions from company data, or automating a recurring process — while never revealing data they are not authorized to view.

2. Development process: from throwaway prototype to production

I did not build this in a single leap. The history shows how it evolved and matured:

StepPRWhat it added
Prototype#7799Throwaway agent-assistant plugin — with only an "agent" data entity and CRUD UI, intended to prove that the plugin scaffolding worked. Explicitly marked DON'T MERGE.
Foundation#7985The real version: erxes-agent plugin — agents, streaming chat, memory, RAG, workflows. +31,277 lines / 201 files. Merged.
Cleanup#7990Resolved issues in 42 files identified by static analysis — removed every any and decomposed overly complex functions.
Configuration#7997Per-agent configurable temperature (fixing an actual model compatibility bug).
Automation#8004Scheduled agent runs — agents triggered on recurring cron schedules. +2,395 lines / 34 files.
Deployment#8006, #8007, #8008Dockerfile, CI image build, release matrix, and CDN publishing — the plumbing that actually gets the plugin onto production servers.

3. Architecture — at a glance

This plugin has two halves, just like every other erxes plugin:

  • erxes-agent_api — the backend, a Node service running on port 3312. Agents, tools, memory, and workflows live here.
  • erxes-agent_ui — the frontend, loaded into the erxes dashboard at runtime as a Module Federation remote.

The backend itself is divided into distinct modules — agent, provider, tool, session, settings, schedule, memory, learning, workflow — each with its own data model and GraphQL schema. The real intelligence lives in the mastra/ layer, which contains the tools, memory, knowledge, workflows, schedules, and files subsystems.

4. Pillar 1 — Multi-provider agents and streaming chat

The core of this plugin is built on Mastra.

Users can create an agent (name, system instructions, selected model) and start a chat. Here is what building this required:

  • Multiple model providers, one interface. Through the Vercel AI SDK, agents can run on Anthropic (Claude), OpenAI, Google, Groq, Mistral, Cohere, or any OpenAI-compatible endpoint—including Kimi / K2. Providers and their actual model lists are fetched at runtime, so new models become available without code changes.
  • Persistent conversations. Each chat session and every turn in it are stored in MongoDB, so conversations survive page refreshes and server restarts without data loss.
  • Small extras that make the product feel complete: LLM-generated thread titles, a chronological trace for each turn, and a one-line live summary showing exactly what the agent is doing right now.
  • File attachments. Users can drag a PDF, Word document, or spreadsheet into the chat; the backend extracts its text (using pdf-parse, mammoth, and exceljs) so the agent can actually read it.

5. Pillar 2 — Advanced Memory (so the agent remembers you)

By default, the agent is stateless — each turn sees only the recent message history. This is safe and simple, but it means the agent forgets everything between sessions. So I added an opt-in advanced memory layer, enabled with a single environment flag.

It consists of two parts:

  1. Semantic recall — every message is converted into an embedding and stored in a vector database. On each new turn, the system finds the most relevant past excerpts and silently adds them to the context. This is precisely what allows the agent to "remember" something you said three sessions ago.
  2. Working memory — a short, agent-maintained profile about you and the task ("prefers to communicate in English", "works on the Flint project"), stored in MongoDB and updated after every turn.

6. Pillar 3 — Company Knowledge RAG (the hard and important part)

This is the capability I am most proud of. The goal: enable the agent to answer questions from the company's entire operational database — contacts, companies, deals, tasks, conversations, and knowledge base articles — keep it fresh as the data changes, and never reveal records the user does not have permission to view.

The core problem is this, and it is subtle:

The moment a record becomes a vector, it loses its permissions. The vector database has no idea who is asking. So access control must be reconstructed at the exact moment of retrieval.

I solved this with a few hard rules:

  1. Store the record's permission attributes on the vector, not the user's. Every vector carries what is required to view that record — tenant (subdomain), content type, owner, branch/department, and whether it is public. It never embeds "user X can view this," because if X's permission is later revoked, the vector would still keep saying "yes" — a data leakage risk.
  2. Resolve the querying user's current permissions at search time. This makes revocation take effect immediately.
  3. Filter twice — first coarse, then definitive (hybrid pre/post filtering). First, a fast pre-filter that runs inside the vector search eliminates ~99% of candidate records by tenant and type. Then a post-filter re-fetches the remaining records through erxes's own resolvers, on behalf of the querying user, in real time — making erxes's actual permission logic the final authority. Nothing is reimplemented; if erxes hides it from you in the UI, it is hidden from the agent too.

Because some of these record types contain personal data, embedding them is explicitly opt-in per type — not a default, but a deliberate data governance decision.

7. Pillar 4 — Live operation registry (zero glue code per plugin)

An agent that can only talk is a toy. To make it take action, it needs tools. The simple (naive) approach would have been to hand-write an integration for each erxes plugin ("this is how you create a deal, this is how you find a contact…") — there are hundreds of them, and they would be perpetually out of date.

I did the opposite. I built a live operation registry: at runtime, it introspects erxes's GraphQL gateway and discovers every operation in every enabled plugin. The agent gets a generic "find a capability" tool backed by that registry, along with a "run this operation" tool.

The agent also has a set of built-in tools: web search (via DuckDuckGo), URL fetching, a calculator, a chart renderer, the companyKnowledge retrieval tool described in Part 6, and tools for reading and running workflows (next section).

8. Pillar 5 — Workflows (crystallized agent behavior)

Chat is powerful but transient — the agent acts only while someone is typing. A business needs processes that run independently for hours, even days. So I designed a workflow kernel.

The insight that made the design clean was this: any business process in any industry can be decomposed into just five core primitives.

PrimitiveMeaningImplemented by
Eventssomething happenedtriggers (any plugin event, time, webhook, manual click)
Capabilitiessomething can be donethe real-time action registry from Section 7
Judgmenta decision requires intelligenceagent step (an LLM that makes a structured decision)
Timea process runs for hours, even weeksdurable runs, waits, schedules
Humanssome decisions belong to peopleapproval step (pause → notify a human → resume)

A workflow is simply data that assembles those five pieces. Customer support, lead nurturing, invoice collection, onboarding — all of them can be expressed without adding a single concept to the kernel. Building on Mastra's workflow engine, I created a small DSL (definition language), compiler, and durable runtime with branching, parallel steps, and a safe condition language.

A workflow is crystallized agent behavior. A process can begin by relying on the agent — "figure out what to do for this event" — and harden over time as predictable decisions are replaced with deterministic steps: cheaper, faster, auditable.

erxes-agent workflow spec

The payoff is that the agent itself can create and run workflows. You describe your process in chat; the agent writes the workflow. This closes the entire loop — create → run → observe → improve — all through conversation. The UI completes the experience with index, detail, and form pages, along with a real-time graphical preview of the workflow.

9. Pillar 6 — Scheduled Agent Runs (#8004)

The final capability: make the agent run a prompt on a recurring cron — "Every Monday at 9 a.m., summarize the new deals from the past week."

The interesting engineering here lies in making this reliable in a system where the queue (Redis) can be cleared at any time:

  • Each enabled schedule becomes a BullMQ job scheduler.
  • A reconciliation job runs every 5 minutes, comparing the actual queue with the schedules stored in MongoDB and adding or removing jobs to make them match.
  • MongoDB is the single source of truth. So if Redis is cleared, the next reconciliation automatically rebuilds every schedule — the system is self-healing.
  • It is multi-tenant-aware (one set of schedules per organization in hosted SaaS mode), and isolates a schedule with a malformed cron expression so it cannot break the others.

10. Shipping to Production — the Unglamorous Last Mile

A merged PR is not a deployed feature. Three subsequent PRs did the foundational work of actually putting the plugin on production servers:

  • #8006 — Dockerfile + CI image build. A two-stage node:22-alpine image (build, then a slim runtime) and a GitHub Actions workflow that publishes it for both Intel and ARM. One gotcha uncovered here: a dependency that had been resolving accidentally thanks to monorepo hoisting had to be declared explicitly; otherwise, the container would crash on startup.
  • #8007 — release matrix. Adding the new API image to the release-tagging pipeline means it now ships with every release.
  • #8008 — CDN publishing + a Module Federation gotcha. The frontend remote is synced to the CDN bucket so the dashboard can load it. Remember the "remote" from Part 3? The plugin folder is erxes-agent_ui (hyphenated), but Module Federation container names cannot contain hyphens — so loadRemote could never find it. The fix preserves the hyphenated folder path while normalizing the runtime remote name to use an underscore. A one-character bug that makes the entire UI invisible in production.

11. Keeping the Code Honest — Quality and Security

Thirty thousand lines is an enormous surface area, so I treated quality as part of the opportunity rather than as an afterthought:

  • Static analysis cleanup (#7990). Running DeepSource flagged the new code; I cleaned up issues across 42 files — eliminating roughly 390 uses of any by introducing real types, and breaking down two overly complex functions (with complexity scores of 31 and 28) into targeted helper functions without changing their behavior.
  • Tests where they matter. Unit and integration tests cover the activity tracker, thread titler, file extraction, three memory behaviors, the workflow compiler and tools, and error sanitization — plus a workflow smoke-test script.
  • Security built in. Ownership checks on every thread and message (you can only access your own data), gateway calls authenticated with Bearer, and structured tool errors — so a failing tool returns a clean message to the agent instead of exposing a stack trace or leaving the user stuck.

12. Future dreams — where could this path lead

12 days is only the beginning. Here are some possible directions for development that can be envisioned on top of the foundation now in place:

  • Voice-enabled agent. Instead of typing, you could pick up your phone and ask "how are today's sales?" and have the agent respond. Multimodal models are already making this possible.
  • Proactive agent. Rather than responding only when asked, it could notice things on its own and make suggestions: "5 of your contracts are about to expire — would you like me to send reminders?" This could grow on top of the learning module.
  • Agent marketplace. Anyone could download ready-made agents and workflows for areas such as sales, support, and finance, and start using them immediately. Because a workflow is simply data, it is easy to share.
  • Multi-agent team. Instead of one agent, several agents, each with a different specialty, could consult with one another and collaborate to complete a large task — just like a real team.
  • Directly to the customer. For now, the agent assists internal employees. Later, it could communicate directly and securely with customers in the support inbox.
  • Fully local, fully private. Just as embedding was handled with a local model, all logic could eventually run on the company's own server, ensuring that no data ever leaves it.
  • Mongolian-language agent. An agent for local businesses that understands and responds perfectly in Mongolian — this is the direction that excites me the most personally.

Any of these can be added without writing a new foundational architecture — because the five core components, real-time activity log, and permission-first RAG are already in place. The hardest part is done; from here, all that remains is to plug in the dream.

13. What I Want You to Take Away from This

  • Build on the architecture you already have. The biggest multiplier was erxes's plugin + GraphQL-gateway design. The real-time action registry turned "integrate the agent with N plugins" into "integrate it zero times, forever." Find your leverage point and build on it.
  • Design from primitives. Reducing any possible business process to five primitives kept the workflow core small and prevented industry-specific oddities from polluting the core.
  • Treat permissions as a retrieval-time problem, not a storage-time problem. This is the difference between a demo and something you can trust with real company data.
  • The last mile is the real mile. Docker, CI, CDN, and a single hyphen in a name determined whether any real user would see any of the clever decisions above.

Related articles