Building AI Agents with CRM Data: A Practical Guide
The nodestash Team
Engineering
The Problem: AI Agents Without Context
AI agents are only as useful as the context they have access to. A support agent that can't look up a customer's history is just a chatbot. A sales assistant that can't see deal stages is just GPT with a sales prompt.
The missing piece is structured data access. LLMs can reason and generate text. What they can't do is query databases, look up customer records, or check deal pipelines — unless you give them tools.
CRM Data as Agent Context
Customer data is some of the most valuable context an AI agent can have:
- Who is this person? Contact details, company, role, tags
- What's their history? Activities, emails, meetings, notes
- Where are they in the pipeline? Deal stage, value, expected close date
- What's custom to them? Lead score, source, industry, plan
Traditional CRMs make this data hard to access programmatically. Rate limits are tight, APIs are inconsistent, and you end up building a caching layer just to give your agent basic lookup capabilities.
Two Approaches
Approach 1: Direct API Integration
The straightforward way. Your agent has tools that call the nodestash API directly.
import { NodeStash } from '@nodestash/sdk'
const crm = new NodeStash({ apiKey: process.env.NODESTASH_KEY })
// Define tools the agent can use
const tools = [
{
name: 'lookup_contact',
description: 'Look up a contact by email address',
parameters: {
email: { type: 'string', description: 'Email address to search for' },
},
execute: async ({ email }: { email: string }) => {
const contacts = await crm.contacts.list({ search: email })
if (!contacts.data.length) return { found: false }
const contact = contacts.data[0]
return {
found: true,
id: contact.id,
name: `${contact.attributes.first_name} ${contact.attributes.last_name}`,
email: contact.attributes.email,
company: contact.attributes.company_id,
tags: contact.attributes.tags,
}
},
},
{
name: 'get_deal_status',
description: 'Get the current deals for a contact',
parameters: {
contact_id: { type: 'string', description: 'Contact ID' },
},
execute: async ({ contact_id }: { contact_id: string }) => {
const deals = await crm.deals.list({ contact_id })
return deals.data.map(deal => ({
title: deal.attributes.title,
value: deal.attributes.value,
stage: deal.attributes.stage_id,
expected_close: deal.attributes.expected_close_date,
}))
},
},
{
name: 'log_interaction',
description: 'Log an AI-assisted interaction as an activity',
parameters: {
contact_id: { type: 'string' },
subject: { type: 'string' },
body: { type: 'string' },
},
execute: async (params: { contact_id: string; subject: string; body: string }) => {
return await crm.activities.create({
type: 'note',
subject: params.subject,
body: params.body,
contact_id: params.contact_id,
is_done: true,
})
},
},
]
This works well when you're building a custom agent framework. You define tools, pass them to the LLM, and handle tool calls.
Approach 2: MCP Server (Model Context Protocol)
MCP is a standard protocol for connecting AI models to data sources. nodestash ships an MCP server that exposes all CRM operations as tools that any MCP-compatible client can use.
// claude_desktop_config.json
{
"mcpServers": {
"nodestash": {
"command": "npx",
"args": ["@nodestash/mcp"],
"env": {
"NODESTASH_API_KEY": "nds_live_..."
}
}
}
}
Once configured, your AI client (Claude, Cursor, or any MCP-compatible tool) can:
- Search and retrieve contacts
- Create and update deals
- Log activities and notes
- Manage pipeline stages
- Query custom fields
No custom integration code. The MCP server translates between the AI model and the nodestash API.
Real-World Patterns
Pattern 1: Customer Support Agent
An AI support agent that looks up customer context before responding.
async function handleSupportTicket(ticket: Ticket) {
// 1. Look up the customer
const context = await tools.lookup_contact({ email: ticket.from_email })
// 2. Get their deal/subscription info
const deals = context.found
? await tools.get_deal_status({ contact_id: context.id })
: []
// 3. Get recent interaction history
const activities = context.found
? await crm.activities.list({
contact_id: context.id,
limit: 5,
})
: { data: [] }
// 4. Build context for the LLM
const systemPrompt = `You are a support agent. Customer context:
Name: ${context.name}
Plan: ${context.tags?.includes('enterprise') ? 'Enterprise' : 'Standard'}
Active deals: ${deals.length}
Recent interactions: ${activities.data.length}
Last contact: ${activities.data[0]?.attributes.created_at ?? 'N/A'}`
// 5. Generate response with context
const response = await llm.complete({
system: systemPrompt,
messages: [{ role: 'user', content: ticket.body }],
})
// 6. Log the AI interaction
if (context.found) {
await tools.log_interaction({
contact_id: context.id,
subject: `AI Support: ${ticket.subject}`,
body: `Query: ${ticket.body}\n\nAI Response: ${response}`,
})
}
return response
}
Pattern 2: Sales Intelligence
An agent that enriches leads and suggests next actions.
async function analyzeLead(contactId: string) {
const contact = await crm.contacts.get(contactId)
const deals = await crm.deals.list({ contact_id: contactId })
const activities = await crm.activities.list({ contact_id: contactId })
const analysis = await llm.complete({
system: 'You are a sales analyst. Analyze this lead and suggest next actions.',
messages: [{
role: 'user',
content: JSON.stringify({
contact: contact.attributes,
deals: deals.data.map(d => d.attributes),
activity_count: activities.data.length,
last_activity: activities.data[0]?.attributes,
}),
}],
})
// Auto-tag based on analysis
await crm.contacts.update(contactId, {
tags: [...contact.attributes.tags, 'ai-analyzed'],
custom_fields: {
ai_summary: analysis,
analyzed_at: new Date().toISOString(),
},
})
return analysis
}
Pattern 3: Automated Data Entry
An agent that processes unstructured data and creates CRM records.
async function processBusinessCard(imageUrl: string) {
// Use vision model to extract contact info
const extracted = await llm.complete({
messages: [{
role: 'user',
content: [
{ type: 'image_url', image_url: { url: imageUrl } },
{ type: 'text', text: 'Extract: name, email, phone, company, title. Return JSON.' },
],
}],
})
const data = JSON.parse(extracted)
// Create or update contact in CRM
const existing = await crm.contacts.list({ search: data.email })
if (existing.data.length > 0) {
return await crm.contacts.update(existing.data[0].id, {
phone: data.phone,
title: data.title,
})
}
return await crm.contacts.create({
first_name: data.first_name,
last_name: data.last_name,
email: data.email,
phone: data.phone,
title: data.title,
tags: ['business-card-scan'],
})
}
Why a CRM API Matters for AI
The key insight: AI agents need structured, queryable data. Not a CSV export. Not a data dump. A real-time API with search, filtering, and the ability to write back.
nodestash was designed for this from day one:
- Fast search — Meilisearch-powered full-text search with typo tolerance
- Custom fields — Store AI-generated metadata directly on CRM records
- Activity logging — Track every AI interaction for audit and training
- Webhooks — Trigger AI workflows when CRM data changes
- Rate limits that make sense — Up to 500 requests/second on Enterprise, with clear headers
Getting Started
- Sign up for nodestash (free tier available)
- Install the SDK:
npm install @nodestash/sdk - Or set up the MCP server:
npx @nodestash/mcp - Start building your agent
The free tier gives you 100 contacts and 1,000 API calls/month — enough to prototype your agent and validate the workflow before scaling up.