Skip to content
Sign up

Building a customer success CRM with the Letta API

Companies generally have human customer success agents that handle dozens or even hundreds of accounts. This is a natural pattern to follow when building out AI-based agents too.

But what if each of your customers had their own dedicated AI-based customer success agent that autonomously researched their background and reached out with personalized communications?

This guide shows you how to programmatically create customer-specific agents using the Letta SDK. Instead of manually setting up agents through a UI, we’ll build an automated workflow that creates a dedicated agent for each customer who signs up for your application. When a customer signs up, their agent immediately springs into action — researching their LinkedIn profile, investigating their company, and sending a personalized welcome email — all without human intervention. This approach positions Letta as a lightweight CRM alternative or a means of augmenting your existing CRM with AI-powered relationship management.

We’ll build a customer sign-up application that demonstrates two key capabilities: autonomous agent action and ongoing conversation.

When someone signs up, their dedicated AI agent immediately:

  • Researches their professional background using web search tools
  • Updates its memory with findings
  • Drafts and sends a personalized welcome email based on its research

After this proactive outreach, the agent remains available for ongoing customer conversations through a chat interface.

To follow along, you need:

  • Node.js 16 or higher: To run the application
  • Git: To clone the starter code repository
  • Letta: To access the agent development platform and API
  • Gmail: To configure the agent’s email sending capability
  • Zapier MCP: To set up the MCP email tool integration
  • Exa: To configure MCP research and LinkedIn lookup tools

Agent templates serve as blueprints that define your agent’s memory structure, tools, and behavior patterns. We’ll configure our template with web research and email tools so agents can autonomously research customers and send personalized communications. You’ll use the Agent Development Environment (ADE) to build and configure your template.

Create and configure a new agent template in the ADE.

The complete template creation process

1. Create a new template

From the Letta dashboard, click Templates+ New templateStart from scratch.

Creating a new agent template in the Letta dashboard

2. Explore the Agent Development Environment

The ADE interface has three key areas:

  • A center chat simulator for testing
  • A left sidebar for configuration (template settings, tools, LLM config)
  • A right panel showing memory architecture with real-time context utilization
Agent Development Environment interface showing chat simulator, configuration sidebar, and memory blocks panel

3. Name your template

Rename your template customer-success-agent by clicking the pen icon next to the Name field.

Editing agent template name in the ADE

Memory blocks create your agent’s persistent knowledge architecture.

In the Core Memory section of the right panel, click Advanced+ New block to add custom memory blocks.

Adding custom memory blocks in Core Memory section

Create four core memory blocks with the following configurations:

Customer block: Customer profile and context

Label: customer

Description:

Detailed information about the customer contact, including professional background, role, and business context

Value:

Customer Profile:
Name: {{customer_name}}
Email: {{customer_email}}
Company: {{company_name}}
Title: {{job_title}}
Industry: {{industry_sector}}
Professional Background:
{{professional_background}}
Business Challenges:
{{business_challenges}}
Communication Preferences:
{{communication_preferences}}

The customer memory block stores everything your agent learns about the individual customer contact. The template variables will be populated when you create agents from this template, and the agents’ research findings accumulate in the descriptive fields.

Organization block: Company knowledge base

Label: organization

Description:

ACME Manufacturing's company information, products, and value propositions (for reference)

Read-only: Yes

Value:

ACME Manufacturing - Industrial Automation Solutions
Key Products:
• QualityCheck AI: Real-time quality control system (reduces defects by 30%)
• SupplyOptimize: Supply chain optimization platform (20% cost savings)
• ProductionFlow: Manufacturing workflow automation
Target Industries: Automotive, Aerospace, Electronics, Medical Devices
Company Size Focus: 100-1000 employees
Value Propositions:
- Reduce manufacturing defects by 25-40%
- Optimize supply chain costs by 15-25%
- Improve production efficiency by 20-35%
- Seamless ERP integration
Competitive Advantages:
- Industry-specific AI models trained on manufacturing data
- 99.7% uptime SLA with 24/7 support
- ROI is typically achieved within 6 months
Contact Information:
- Representative: [Your Name]
- Title: [Your Title]

The organization memory block contains static company information that agents reference but shouldn’t modify. Set as read-only to ensure consistent messaging across all customer interactions.

Tool use guidelines block: Research and communication guidelines

Label: tool_use_guidelines

Description:

Instructions for effective tool usage and customer research methodology

Read-only: Yes

Value:

Research Strategy:
1. Begin with LinkedIn research to understand the contact's professional background
2. Use web search to research the customer's company, recent news, and industry challenges
3. Focus on identifying specific use cases for ACME Manufacturing's solutions
4. Look for relevant business triggers (growth, challenges, new initiatives)
Communication Guidelines:
- Personalize emails based on research findings
- Reference specific company challenges or industry trends
- Connect ACME products to the customer's likely pain points
- Maintain a professional but approachable tone
- Include relevant case studies or statistics when appropriate
Tool Usage Best Practices:
- Use company_research_exa for comprehensive business intelligence
- Use linkedin_search_exa for professional background verification
- Draft emails with research context before sending
- Update memory blocks with key findings after each research session

The tool_use_guidelines memory block contains instructions for how your agent should use its research and communication tools effectively.

Tasks block: Action items and objectives

Label: tasks

Description:

Current objectives, action items, and next steps for this customer relationship

Value:

Current Priorities:
1. Complete customer and company research
2. Identify specific use cases for ACME products
3. Draft personalized welcome email
4. Schedule a discovery call
5. Prepare customized demo materials
Research Checklist:
- [] LinkedIn professional background research
- [] Company website and recent news analysis
- [] Industry-specific challenge identification
- [] Competitive landscape assessment

The tasks memory block contains specific customer onboarding tasks, like research, email drafting, and discovery call scheduling, with a checklist for tracking task completion.

Understand template variables

The {{variable_name}} placeholders in the customer memory block are populated with actual customer data when you create agents from the template. You enter these variables programmatically via the SDK when creating agents in the application code.

Tools transform your agent from a static responder into an active researcher that autonomously gathers information and takes action. In the left sidebar of the ADE, click ToolsTool Manager. This opens a modal where you’ll configure Model Context Protocol (MCP) servers that provide external capabilities for agents.

Tool manager modal showing MCP server configuration options

Your agent needs research tools for looking up customer backgrounds and company information, and email tools for automatically drafting and sending personalized welcome messages.

Configure the Exa MCP server

Exa provides web search, company research, and LinkedIn lookup capabilities for your agent.

  1. In the MCP servers section of the Tool manager sidebar, click + Add MCP server. Select Exa from the list to open the configuration modal.

  2. Go to exa.ai to create an account. Then visit dashboard.exa.ai/api-keys to copy and save your default secret key.

    Exa API key configuration in dashboard
  3. Paste the Exa MCP server URL, https://mcp.exa.ai/mcp?exaApiKey=your-exa-api-key, in the Server URL field on the modal. Replace your-exa-api-key with your Exa secret key and click Test connection.

  4. When connected, the modal displays a list of available tools, including web_search_exa, company_research_exa, and linkedin_search_exa. Click Confirm to add the server.

    Available Exa tools after successful connection
Configure the Zapier MCP server

Zapier provides Gmail integration for drafting and sending personalized emails.

  1. Click + Add MCP server again and select Zapier.

  2. For the API setup, go to mcp.zapier.com/mcp, create an account, then click + New MCP Server in your Zapier dashboard.

    Creating new MCP server in Zapier dashboard
  3. Choose Other from the MCP Client options and name it Email sender. After creating the server, click + Add tool, select Gmail from the modal, and add the Gmail tools. Connect your Gmail account for sending emails.

    Adding Gmail tools in Zapier MCP server
  4. In your Zapier dashboard, click Connect in the top toolbar and copy the Server URL.

    Copying server URL from Zapier dashboard
  5. Return to the Letta modal, paste the server URL in the Server URL field, and test the connection. When you see the Gmail tools appear, click Confirm.

    Gmail tools successfully connected in Letta
Connect tools to the template

In the Tool manager view, you can now see both MCP servers. Click each server to view the available tools. To attach them, click the link icon next to the tools your agent needs:

Attaching tools to agent in Tool Manager

From Exa: Attach web_search_exa, company_research_exa, and linkedin_search_exa.

From Zapier: Attach gmail_create_draft and gmail_send_email.

Once you’ve configured all memory blocks and tools, save your template to make it available for programmatic agent creation.

Save and version your template

Click the Save button in the ADE toolbar. This opens a version modal. Click Save new version to save your template for agent creation.

Your customer-success-agent template is now ready. You can use it to create customer-specific agents programmatically via the SDK.

Step 2: Set up the application and install the Letta SDK

Section titled “Step 2: Set up the application and install the Letta SDK”

Now that we’ve configured our agent template, we’ll set up a customer sign-up application and integrate it with the Letta SDK.

Clone and explore the starter application

1. Clone the repository and install dependencies

Clone the starter repository and install its dependencies:

Terminal window
git clone https://github.com/letta-ai/letta-customer-specific-agents-starter.git
cd letta-customer-specific-agents-starter
npm install

The starter application includes:

  • A sign-up flow that collects the customer’s name, email, company, and job title
  • A login system that uses email-based authentication
  • A chat interface with a placeholder chatbot that echoes messages
  • User storage with JSON-based persistence in users.json

2. Start and test the application

Start the application to verify everything works:

Terminal window
npm start

Open http://localhost:3000 in your browser. Click Sign Up and create a test account. After signing up, you’re redirected to the chat interface. Try sending a message – the chatbot will echo it back. This echoing is a placeholder behavior that we’ll replace with the Letta agent integration.

Customer success agent chat interface showing conversation with the agent

3. Understand the application structure

The application structure looks like this:

customer-success-crm-starter/
├── server.js # Express server with API routes
├── public/
│ ├── index.html # Homepage with login form
│ ├── signup.html # Sign-up form
│ ├── chat.html # Chat interface
│ └── style.css # Styling
├── users.json # User data storage (auto-generated)
└── package.json # Dependencies

The server.js file contains four API endpoints:

  • POST /api/signup creates new user accounts
  • POST /api/login authenticates users by email
  • GET /api/user/:userId retrieves user information
  • POST /api/chat handles chat messages (currently by echoing them)

With the starter application running, we’re ready to install the Letta SDK and integrate it to automatically create agents for each new user.

Install the Letta SDK and Dotenv for environment variable management:

Terminal window
npm install @letta-ai/letta-client dotenv

This installs:

  • @letta-ai/letta-client: The official Letta SDK for Node.js
  • dotenv: Dotenv for loading environment variables from a .env file

Create a .env file in the project root:

Terminal window
touch .env

Open .env and add your Letta API configuration:

LETTA_API_KEY=your_letta_api_key_here
LETTA_TEMPLATE_VERSION=your_project/customer-success-agent:latest

To get your Letta API key, navigate to app.letta.com/api-keys in your browser. Click + Create API key, name it (for example, CRM Integration), and copy the key value.

For the LETTA_TEMPLATE_VERSION, you need the template identifier from your Letta dashboard. Navigate to Templates in the Letta dashboard and click on your customer-success-agent template. The template version follows the format {project_slug}/{template_name}:{version}.

For example, if your project slug is default-project, the value would be:

LETTA_TEMPLATE_VERSION=default-project/customer-success-agent:latest

Now initialize the Letta SDK in server.js. First, add the Dotenv import at the very top of the file (before the existing imports):

require('dotenv').config();

Then, add the Letta client import after the path import:

const { Letta } = require('@letta-ai/letta-client');

Finally, add the Letta client initialization after the USERS_FILE constant declaration:

// Initialize Letta client
const client = new Letta({
apiKey: process.env.LETTA_API_KEY,
});

The Letta instance connects to the Letta API using your API key. We’ll use this client throughout the application to create identities, create agents, and send messages.

When a customer signs up, we need to create a Letta identity for them. An identity represents a unique user in the Letta system and serves as the foundation for creating dedicated agents. The identity–agent relationship enables proper multi-user isolation — each customer gets their own identity, and each identity can have one or more agents associated with it.

We’ll modify the sign-up endpoint to create agents in the background, while allowing users to proceed immediately. This prevents sign-up delays while agent creation completes asynchronously.

In the POST /api/signup endpoint, find the newUser object (around line 60) and add three new fields to track agent creation:

const newUser = {
id: Date.now().toString(),
name,
email,
company,
role,
agentId: null, // Add this
identityId: null, // Add this
agentStatus: 'creating', // Add this
createdAt: new Date().toISOString()
};

Add the following code after the res.json() call (before the closing } catch block) to trigger background agent creation:

// Create agent asynchronously in background (don't await)
createAgentForUser(newUser.id, name, email, company, role).catch(error => {
console.error(`Failed to create agent for user ${newUser.id}:`, error);
});

This approach creates the user account immediately, returns a success response, and then triggers background agent creation without blocking the sign-up flow.

Now add the background function that handles identity and agent creation. Add the following function after the sign-up endpoint (before the login endpoint):

// Background function to create agent
async function createAgentForUser(userId, name, email, company, role) {
try {
console.log(`[Background] Creating identity for ${name}...`);
const identity = await client.identities.create({
identifier_key: `customer_${Date.now()}`,
name: name,
identity_type: "user"
});
console.log(`[Background] Identity created with ID: ${identity.id}`);
// Agent creation will go here in Step 4
// Update user with identity and agent info
const users = await readUsers();
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex !== -1) {
users[userIndex].identityId = identity.id;
users[userIndex].agentStatus = 'ready';
await writeUsers(users);
}
} catch (error) {
console.error(`[Background] Error creating agent for user ${userId}:`, error);
// Update user status to error
const users = await readUsers();
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex !== -1) {
users[userIndex].agentStatus = 'error';
await writeUsers(users);
}
}
}

The identifier_key uses a timestamp to ensure uniqueness. In a production application, you might use UUIDs or identifiers from your existing CRM system.

The background function creates the identity and updates the user record with the identity ID. In the next step, we’ll add the agent creation logic.

With the identity created, we can now create a dedicated agent from our customer-success-agent template. The template-based approach creates a fully-configured agent with all memory blocks, tools, and settings in a single API call.

Update the createAgentForUser function to add agent creation after identity creation. Replace the // Agent creation will go here in Step 4 comment with the following code:

console.log(`[Background] Creating Letta agent from template for ${name}...`);
let agent;
const templateVersion = process.env.LETTA_TEMPLATE_VERSION;
if (templateVersion) {
console.log(`[Background] Using template: ${templateVersion}`);
const response = await client.templates.agents.create(templateVersion, {
identityIds: [identity.id],
memoryVariables: {
customer_name: name,
customer_email: email,
company_name: company,
job_title: role
}
});
// API returns agent IDs, retrieve the full agent object
agent = await client.agents.retrieve(response.agentIds[0]);
} else {
console.log(`[Background] No template configured, creating agent manually...`);
agent = await client.agents.create({
memory_blocks: [
{
label: "customer",
value: `Customer Profile:
Name: ${name}
Email: ${email}
Company: ${company}
Title: ${role}
Professional Background:
[To be researched and updated by the agent]
Business Challenges:
[To be identified through conversations]
Communication Preferences:
[To be learned over time]`
},
{
label: "persona",
value: "I am a dedicated customer success agent. My role is to help customers get the most value from our product, understand their needs, and provide personalized support. I should be professional, helpful, and proactive in identifying opportunities to assist."
}
],
model: "openai/gpt-4o-mini",
embedding: "openai/text-embedding-3-small",
identityIds: [identity.id]
});
}
console.log(`[Background] Agent created with ID: ${agent.id}`);

The code checks whether LETTA_TEMPLATE_VERSION is configured. If it is, the agent is created from your template with memory variables populated from the sign-up form. The template version string (like default-project/customer-success-agent:latest) is passed directly to the SDK. The API returns agent IDs, so we retrieve the full agent object with a separate call.

The memoryVariables object maps sign-up form data to the template variables we defined in Step 1. These values populate the {{variable_name}} placeholders in the agent’s customer memory block. Fields like professional_background and business_challenges remain empty – the agent will research and populate these using its tools.

If no template is configured, the code falls back to manual agent creation with basic memory blocks. This fallback won’t include the research tools or additional memory blocks from your template, but it allows testing without template configuration.

Now update the section that saves the agent information to the user record. Find the comment // Update user with identity and agent info and replace that entire block with the following code:

// Update user with identity and agent info
const users = await readUsers();
const userIndex = users.findIndex(u => u.id === userId);
if (userIndex !== -1) {
users[userIndex].agentId = agent.id;
users[userIndex].identityId = identity.id;
users[userIndex].agentStatus = 'ready';
await writeUsers(users);
}

Now that we’ve created the agent and linked it to the customer’s identity, we’ll send an initial prompt to activate it and have it begin researching the customer.

After the user update code, add this code to send the initial research prompt:

// Send initial prompt to agent for research and welcome email
console.log(`[Background] Sending initial research prompt to agent...`);
try {
await client.agents.messages.create(agent.id, {
messages: [{
role: "system",
content: `You are a customer success agent assigned exclusively to ${name} (${email}) at ${company}. They are a ${role}. From this point forward, all messages you receive will be from ${name} unless otherwise specified.
Your first task: Research ${name}'s background on LinkedIn and their company through search, update your customer memory block with key findings, draft a personalized welcome email based on your research that connects ACME's automation solutions to their manufacturing challenges, and send the email immediately.`
}]
});
console.log(`[Background] Initial agent prompt sent successfully`);
} catch (promptError) {
console.error(`[Background] Warning: Could not send initial prompt:`, promptError.message);
}

This initial prompt triggers autonomous agent behavior. The agent will:

  • Use the linkedin_search_exa tool to research the customer’s professional background
  • Use the company_research_exa tool to investigate their company and industry
  • Update its customer memory block with its findings
  • Use gmail_create_draft to compose a personalized welcome email incorporating its research insights
  • Use gmail_send_email to send the email immediately

The agent completes this entire workflow without human intervention. The customer receives a personalized welcome email within minutes of signing up, demonstrating how agents can provide value before any direct conversation begins. After sending the email, the agent remains available for ongoing interactions through the chat interface.

Step 5: Integrate the chat endpoint with Letta agents

Section titled “Step 5: Integrate the chat endpoint with Letta agents”

The final integration step replaces the placeholder echo chatbot with real Letta agent communication. Each user’s messages will be sent to their dedicated agent, and the agent’s responses will be displayed in the chat interface.

Find the POST /api/chat endpoint in server.js and replace the entire endpoint with the following code:

app.post('/api/chat', async (req, res) => {
try {
const { userId, message } = req.body;
if (!userId || !message) {
return res.status(400).json({ error: 'User ID and message are required' });
}
const users = await readUsers();
const user = users.find(u => u.id === userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
if (!user.agentId) {
return res.status(400).json({ error: 'No agent associated with this user' });
}
// Send message to the user's dedicated Letta agent
console.log(`Sending message to agent ${user.agentId} from identity ${user.identityId}...`);
const agentResponse = await client.agents.messages.create(user.agentId, {
messages: [
{
role: 'user',
content: message,
senderId: user.identityId // Link message to the user's identity
}
]
});
// Extract the assistant's response from the messages
let responseText = '';
for (const msg of agentResponse.messages) {
if (msg.message_type === 'assistant_message') {
responseText = msg.content;
break;
}
}
res.json({
success: true,
response: responseText || 'I received your message but need a moment to formulate a response.'
});
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({
error: 'Server error',
details: error.message
});
}
});

The endpoint now sends each message to the user’s dedicated agent via client.agents.messages.create(). The senderId parameter links the message to the user’s identity, which enables proper user attribution in Letta’s system and allows the agent to track which customer is sending each message.

Agent responses can contain multiple messages – function call results, internal reasoning, and the final assistant message. We loop through the response messages to find the one with message_type: 'assistant_message', which contains the text that should be displayed to the user.

The endpoint includes validation to check that the user exists and has an associated agent. If agent creation is still in progress (the background function in Step 3), the error message indicates that no agent is available yet.

With all integrations complete, restart the server to test the full workflow:

Terminal window
npm start

Open http://localhost:3000 in your browser and click Sign Up. Fill in the form with your own details – use your real name, email, company, and job title. The agent will research this information and send you an email, so using real data provides the most realistic testing experience.

After clicking Sign Up, you’ll be redirected to the chat interface immediately. The sign-up completes instantly while agent creation and autonomous research happen in the background. Watch your terminal output – you should see logs showing the identity creation, agent creation, and initial prompt delivery:

[Background] Creating identity for John Doe...
[Background] Identity created with ID: abc-123...
[Background] Creating Letta agent from template for John Doe...
[Background] Using template: default-project/customer-success-agent:latest
[Background] Agent created with ID: agent-456...
[Background] Initial agent prompt sent successfully

When you see Initial agent prompt sent successfully, you know your agent is researching your background and drafting a welcome email. Check your email inbox – within a few minutes, you should receive a personalized welcome message from your agent that references your professional background and company context.

The welcome email demonstrates the agent’s autonomous research capabilities. It will mention specific details about your role, company, or industry that it discovered through LinkedIn and web search.

After receiving the email, return to the chat interface and send a message like:

Hi, what do you know about me?

The agent will respond with information from its memory, including details it gathered during its research. The agent maintains context across the conversation and remembers everything from its initial research.

To verify persistence, log out and log back in using your email. The agent should remember your previous conversation and maintain its memory across sessions.

You’ve created a working customer success system with programmatic agent creation. Consider the following ways to scale this into a production CRM or integrate it with your existing customer relationship tools:

Integrating with existing CRM systems

Connect your application to CRM platforms like HubSpot, Salesforce, or Pipedrive using webhooks. When a new contact is created in your CRM, automatically trigger agent creation:

app.post('/webhook/crm-contact-created', async (req, res) => {
const { name, email, company, title } = req.body;
// Create identity and agent automatically
createAgentForUser(null, name, email, company, title);
res.json({ success: true });
});

This approach augments your CRM with AI-powered relationship management – each customer gets a dedicated agent that researches their background, maintains conversation history, and provides personalized support. The agent can update the CRM with insights from conversations, qualifying leads and tracking engagement automatically.

Forward customer emails to agents using Zapier integration

Set up automatic email forwarding so your agents can handle ongoing customer communications beyond the initial welcome. When customers reply to emails or send new messages, Zapier can route them to their dedicated agents, allowing the agents to update their memory with new information, draft contextual responses based on conversation history, and maintain personalized interactions across multiple touchpoints. This transforms your agents from one-time onboarding tools into continuous relationship managers.

Deploying to production

For production deployments, replace the JSON file storage with a database (PostgreSQL, MySQL, or MongoDB) to handle concurrent access and larger user bases. Implement a queue system (like BullMQ or AWS SQS) for agent creation to handle spikes in sign-ups and provide retry logic for failed agent creation attempts.

Add monitoring and logging using services like Datadog or CloudWatch to track agent creation success rates, response times, and API errors. Store API keys and template versions in environment variables using a secrets manager like AWS Secrets Manager or HashiCorp Vault.

Consider implementing template versioning to upgrade all customer agents when you improve your agent template. The Letta SDK supports programmatic template updates that can migrate existing agents to new template versions.

Other possibilities to explore:

  • Agents that schedule meetings and automatically manage follow-ups
  • Integration with support ticket systems for context-aware assistance
  • Analytics dashboards that track relationship progression and agent effectiveness
  • Multi-channel support (such as Slack, Discord, and SMS) for customer communications

The combination of programmatic agent creation, persistent memory, and research tools provides a foundation for building sophisticated customer relationship systems that scale. This approach positions Letta as a lightweight alternative to traditional CRMs or as an intelligent layer on top of existing customer data platforms.