DEV Community

Jestony Silvano
Jestony Silvano

Posted on

How I Built an AI Assistant for My SaaS CRM with Scoped Agents

How I Built an AI Assistant for My SaaS CRM with Scoped Agents

When I first started adding AI to my SaaS CRM, the obvious idea was simple:

Give the AI access to the CRM data and let users talk to it naturally.

But the more I worked on it, the more I realized that "an AI assistant for my CRM" isn't really one problem.

A CRM has different users, organizations, roles, private data, public documentation, and actions that can change data.

A personal assistant shouldn't have the same context as an organization assistant. A public product assistant definitely shouldn't have access to a customer's CRM records.

So instead of building one giant AI assistant, I designed uniThread's AI system around scoped agents.

The goal was simple:

Give each agent only the context, knowledge, and tools it actually needs.

This is how I built it.


The problem with one giant AI agent

Imagine having one AI agent with access to everything:

                    AI Agent
                       │
        ┌──────────────┼──────────────┐
        │              │              │
    User Data      CRM Data       Platform Docs
        │              │              │
        └──────────────┼──────────────┘
                       │
                    Tools
                       │
                   Database
Enter fullscreen mode Exit fullscreen mode

At first, this looks convenient.

The model can answer more questions and perform more actions because it has access to more information.

But it creates a much bigger authorization problem.

What happens when a user asks:

"Show me the contacts in my organization."

That's different from:

"What is uniThread?"

And both are different from:

"Create a task for John tomorrow."

The AI needs to know not only what the user is asking, but also what the user is allowed to access and what the application is allowed to execute.

This is where I started separating the system into scoped agents.


What are scoped agents?

Instead of having one agent responsible for everything, I defined agents around different contexts.

The simplified architecture looks like this:

                         User
                           │
                           ▼
                     AI Request
                           │
                           ▼
                      Agent Scope
                           │
          ┌────────────────┼────────────────┐
          │                │                │
          ▼                ▼                ▼
   Personal Agent   Organization Agent   CRM Agent
          │                │                │
          ▼                ▼                ▼
   Personal Context   Org Context      Platform Context
          │                │                │
          └────────────────┼────────────────┘
                           ▼
                     Available Tools
                           │
                           ▼
                      Authorization
                           │
                           ▼
                    Application / DB
Enter fullscreen mode Exit fullscreen mode

The important part is that the agent isn't just a different system prompt.

The scope affects the context and capabilities available to the agent.


The three agent contexts

For uniThread, I currently separate the AI system into three main contexts.

1. Personal Assistant

The personal assistant operates around an individual user's context.

For example, this could eventually handle things such as:

  • Personal tasks
  • Personal notes
  • User-specific information
  • Personal workflows

The important part is that its context belongs to the user.

User
  │
  ▼
Profile
  │
  ▼
Personal Assistant
  │
  ▼
Personal Context
Enter fullscreen mode Exit fullscreen mode

It shouldn't automatically gain access to an entire organization's CRM just because the user is logged in.


2. Organization Assistant

The organization assistant works within an organization's context.

A user's organization membership and role become important here.

For example:

User
  │
  ▼
Organization Membership
  │
  ├── Organization ID
  ├── Member ID
  └── Role
          │
          ▼
   Organization Assistant
Enter fullscreen mode Exit fullscreen mode

The assistant can then work with organization-level CRM capabilities, subject to the same authorization rules as the rest of the application.

This is especially important in a multi-tenant SaaS application.

A user belonging to Organization A shouldn't be able to use the AI to retrieve information from Organization B.

The AI doesn't get to bypass the application's tenant boundaries just because the request is expressed in natural language.


3. CRM Assistant

The third agent is the public-facing CRM assistant for uniThread.

This one has a very different purpose.

It's designed to answer questions about the platform itself using documentation and other indexed knowledge.

For example:

"What does uniThread do?"

or:

"How does lead management work?"

This agent uses retrieval rather than accessing a customer's private CRM records.

The architecture is roughly:

User Question
      │
      ▼
   CRM Agent
      │
      ▼
 Retrieve Relevant Docs
      │
      ▼
    Context
      │
      ▼
     LLM
      │
      ▼
 Answer + Citations
Enter fullscreen mode Exit fullscreen mode

This separation is important because knowledge about the product is not the same thing as access to customer data.


The context passed into the AI system

To keep the agent aware of its execution context, I use an agent context similar to:

type AgentContext = {
  profileId?: string;
  orgId?: string;
  memberId?: string;
  role?: "owner" | "manager" | "agent";
  accessToken?: string;
  isPublic: boolean;
};
Enter fullscreen mode Exit fullscreen mode

Not every agent needs every field.

For example, a public CRM assistant doesn't need a customer's organization ID.

An organization assistant does.

A personal assistant may primarily care about the user's profile.

This gives the application a structured way to describe:

Who is making this request, what organization are they operating in, what role do they have, and what kind of AI interaction is this?


The architecture

I didn't want my controller to contain all of the AI logic.

The system is separated into several layers:

Controller
    │
    ▼
AIAgentService
    │
    ▼
Orchestrator
    │
    ▼
Model Router
    │
    ▼
Agent / Model Adapter
    │
    ▼
Tools
    │
    ▼
Authorization
    │
    ▼
Execution
Enter fullscreen mode Exit fullscreen mode

Each layer has a different responsibility.

Controller

Receives the request and handles the API boundary.

AIAgentService

Handles the higher-level AI interaction.

Orchestrator

Coordinates the agent flow, context, model, tools, and execution.

Model Router

Allows the application to choose the appropriate model or provider without coupling the rest of the system directly to one model.

Agent / Model Adapter

Handles the model-specific implementation.

Tools

Expose controlled application capabilities to the model.

Authorization

Determines whether the requested operation is actually allowed.

Execution

Performs the approved operation against the application or database.

This separation became increasingly important as the AI functionality grew.


The most important distinction: AI is not authorization

One of the biggest things I learned while building this system is:

The LLM should never be the final authority on what a user is allowed to do.

A model can interpret intent.

It can decide that a user's request probably corresponds to create_task.

But it shouldn't decide whether the user is actually allowed to create that task.

The flow should look more like:

User Request
     │
     ▼
     LLM
     │
     │ "I want to call create_task"
     ▼
Tool Request
     │
     ▼
Authorization
     │
     ├── Not allowed → Reject
     │
     ▼
Validation
     │
     ▼
Confirmation if required
     │
     ▼
Execution
Enter fullscreen mode Exit fullscreen mode

The model proposes an action.

The application decides whether that action can happen.

That distinction is critical.


Giving the agent tools

Instead of giving the model unrestricted access to my backend or database, I expose specific tools.

Some examples in my CRM assistant architecture include:

search_contacts
create_note
create_task
Enter fullscreen mode Exit fullscreen mode

This gives the model a controlled interface.

For example, if the user says:

"Find John from Acme."

The model can determine that it needs the contact search capability.

If the user says:

"Create a follow-up task for John tomorrow."

The model can determine that create_task is relevant.

The model doesn't need to know how my PostgreSQL tables work.

It doesn't need raw database credentials.

It doesn't need unrestricted SQL access.

It only needs the capabilities that the application intentionally exposes.


Read operations vs mutations

Not every tool should be treated the same way.

There's an important difference between:

"Find my contact named John."

and:

"Create a task for John tomorrow."

The first is a read operation.

The second changes application state.

That means mutations need stronger controls.

A simplified flow can look like this:

Read:

User
 ↓
AI
 ↓
Tool
 ↓
Authorization
 ↓
Result
Enter fullscreen mode Exit fullscreen mode

For a mutation:

User
 ↓
AI
 ↓
Tool Request
 ↓
Authorization
 ↓
Validation
 ↓
Confirmation
 ↓
Execution
Enter fullscreen mode Exit fullscreen mode

The exact confirmation rules can vary by operation, but the underlying idea is the same:

Don't let a language model silently perform important state-changing actions just because it generated a tool call.


Scoped tools

The agent scope also affects which capabilities are available.

Conceptually, the system can look like:

Personal Agent
 ├── Personal Tasks
 └── Personal Notes

Organization Agent
 ├── Contacts
 ├── Tasks
 ├── Notes
 └── Other CRM Operations

Public CRM Agent
 └── Platform Documentation Search
Enter fullscreen mode Exit fullscreen mode

This means the public CRM assistant doesn't suddenly get access to:

search_contacts
create_task
create_note
Enter fullscreen mode Exit fullscreen mode

for a customer's private organization.

Its job is to answer questions about the platform, not operate someone's CRM.

That's a much smaller and more controlled problem.


Scoped agents are not a replacement for authorization

This is probably the most important security point in the architecture.

Having an agent called organization-assistant doesn't magically make it secure.

You still need authorization underneath it.

My application already has organization and role boundaries, and those boundaries need to continue applying when requests come from an AI agent.

The architecture therefore becomes layered:

AI Agent Scope
      │
      ▼
Application Authorization
      │
      ▼
Organization / Member Scope
      │
      ▼
Database Security
      │
      ▼
Data
Enter fullscreen mode Exit fullscreen mode

The AI layer is another interface into the application.

It shouldn't become a shortcut around the application's existing security model.


How this connects to PostgreSQL RLS

This is also where my previous work on PostgreSQL Row Level Security became important.

The AI system sits above the database security layer.

For example:

User
 ↓
AI Agent
 ↓
Tool
 ↓
Application Authorization
 ↓
PostgreSQL
 ↓
RLS
 ↓
Tenant Data
Enter fullscreen mode Exit fullscreen mode

The application can validate the request, while PostgreSQL RLS provides another boundary at the data layer.

This gives the system defense in depth.

If you're interested in the database side of this architecture, I wrote about it in my previous article:

How I Built Multi-Tenant Data Isolation for My SaaS CRM with PostgreSQL RLS

The two problems are closely related.

The first is:

How do I keep tenants from accessing each other's data?

The second is:

How do I let AI interact with that data without bypassing those boundaries?


RAG for the public CRM assistant

For the public CRM assistant, I also needed a way to answer questions about uniThread without relying entirely on the model's built-in knowledge.

That's where RAG comes in.

The basic flow is:

Documentation
     │
     ▼
Chunking
     │
     ▼
Embeddings
     │
     ▼
Vector Search
     │
     ▼
Relevant Context
     │
     ▼
LLM
     │
     ▼
Answer
Enter fullscreen mode Exit fullscreen mode

Instead of asking the model to simply "know" how uniThread works, the application retrieves relevant documentation and provides that information as context.

This also gives me a better path toward citations and reduces the chance of the model inventing product information.

Most importantly, the RAG knowledge base is separate from private customer CRM data.

That's another example of why scope matters.


What happens when something goes wrong?

AI systems don't always behave exactly as expected.

A user might ask for something outside the agent's scope.

The model might choose an inappropriate tool.

A tool might receive invalid parameters.

The user might not have permission to perform the requested action.

The database operation might fail.

So I don't treat:

LLM → Database
Enter fullscreen mode Exit fullscreen mode

as a valid architecture.

Instead, there are several boundaries:

LLM
 ↓
Tool Selection
 ↓
Input Validation
 ↓
Authorization
 ↓
Confirmation
 ↓
Business Logic
 ↓
Database
Enter fullscreen mode Exit fullscreen mode

Each layer gets an opportunity to reject an invalid operation.

This makes the AI system behave more like another application client rather than a privileged superuser.


What I learned building it

The biggest lesson was that adding AI to a SaaS application is not mainly about calling an LLM API.

The difficult part is everything around the model.

I had to think about:

  • Who is making the request?
  • What context does the agent need?
  • What data can it access?
  • What tools should it have?
  • What actions can it perform?
  • Which operations require confirmation?
  • How does tenant isolation continue to work?
  • How do I prevent the AI layer from bypassing authorization?
  • How do I provide product knowledge without exposing private customer data?

The model is only one component of that system.


The architecture today

The resulting architecture looks roughly like this:

                         User
                           │
                           ▼
                      AI Request
                           │
                           ▼
                     Agent Context
                           │
                           ▼
                       Orchestrator
                           │
             ┌─────────────┼─────────────┐
             │             │             │
             ▼             ▼             ▼
        Personal       Organization     CRM
         Agent            Agent         Agent
             │             │             │
             └─────────────┼─────────────┘
                           │
                           ▼
                      Model Router
                           │
                           ▼
                       LLM / Model
                           │
                           ▼
                         Tools
                           │
                           ▼
                     Authorization
                           │
                           ▼
                       Validation
                           │
                    ┌──────┴──────┐
                    │             │
                 Read          Mutation
                    │             │
                    │        Confirmation
                    │             │
                    └──────┬──────┘
                           ▼
                    Business Logic
                           │
                           ▼
                       Database
                           │
                           ▼
                         RLS
Enter fullscreen mode Exit fullscreen mode

It's still evolving, but this structure gives me something much more useful than a single chatbot bolted onto the side of the CRM.


Final thoughts

Building an AI assistant inside a SaaS product changed how I think about AI architecture.

The interesting question isn't just:

"Which model should I use?"

It's:

"What should this model be allowed to know and do?"

For me, scoped agents became a practical way to answer that question.

Each agent gets a defined purpose, context, and set of capabilities. The application remains responsible for authorization, validation, and execution.

The result is an AI system that can interact with a real SaaS application without treating the LLM as an all-powerful user.

And that's probably the biggest lesson I've taken from building it:

AI doesn't replace application architecture. It makes good application architecture even more important.

uniThread is still evolving, and so is its AI architecture. But building it has been a great way to learn what happens when LLMs move beyond chat and start becoming actual components of a software system.

Top comments (0)