Custom AI Agent
Build a custom AI agent with a tailored personality, Shopify data access, and publish it to the Reeve Marketplace.
Build a Custom AI Agent for Your Store
The agent-app template lets you ship a specialized AI agent that lives in the Reeve Cockpit alongside the built-in assistants. Merchants install your agent from the Marketplace and talk to it just like they do with Reeve's native agents — but your agent has a custom personality, specialized knowledge, and tools you define.
In this tutorial, you'll build Store Concierge — a DTC brand assistant that knows your Shopify data, answers questions about orders and products, and gives revenue summaries in plain English.
What you'll build:
- A custom chat UI with streaming responses
- A system prompt that gives the agent personality + store context
- Tool calling: the agent fetches live Shopify data when asked
- A "Quick questions" menu for common queries
APIs used: reeve.ai.stream, reeve.data.query, reeve.events
Time: ~25 minutes
1. Scaffold with the Agent Template
npx create-reeve-app store-concierge --template agent-app
cd store-concierge
npm install
npm run devThe agent-app template includes a pre-built chat UI skeleton. You'll customize the system prompt, add Shopify tool calling, and style it.
Update reeve-manifest.json:
{
"id": "com.yourcompany.store-concierge",
"name": "Store Concierge",
"version": "1.0.0",
"description": "Your AI store assistant — knows your Shopify data, answers questions instantly.",
"longDescription": "Store Concierge is a specialized AI agent for DTC brands. Ask about revenue trends, top products, recent orders, and customer insights. Get instant answers backed by your live Shopify data — no more exporting CSVs.",
"author": {
"name": "Your Name",
"email": "you@yourcompany.com"
},
"category": "ai-tools",
"tags": ["agent", "shopify", "analytics", "assistant", "chat"],
"permissions": [
"ai.complete",
"ai.stream",
"data.shopify",
"storage.read",
"storage.write",
"events.subscribe"
],
"entrypoint": "index.html",
"icon": "public/icon.png",
"pricing": {
"model": "subscription",
"price": 29,
"currency": "USD",
"interval": "month",
"trial": { "days": 14 }
},
"support": {
"email": "support@yourcompany.com"
}
}2. Design the Agent Architecture
The agent works in two phases for data questions:
- Intent detection — classify whether the question needs live data
- Data fetch + answer — if yes, query Shopify and stream an answer with real numbers
User: "What were my top 3 products this week?"
│
▼
Intent: data_required (source: shopify, type: products)
│
▼
reeve.data.query({ source: 'shopify', type: 'products', ... })
│
▼
reeve.ai.stream(prompt + data context) → streamed answerFor conversational questions that don't need data (e.g., "What should I name my new collection?"), the agent skips the data fetch.
3. Build the Agent Core
Create src/agent.ts:
import { reeve } from '@reeve/app-sdk';
export type Message = {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
};
const SYSTEM_PROMPT = `You are Store Concierge, a specialized AI assistant for DTC ecommerce brands.
Your personality:
- Friendly, direct, and data-driven
- You love talking about revenue, products, and customers
- You celebrate wins with the merchant ("That's a great AOV!")
- You're honest about limitations — if data isn't available, say so clearly
Your capabilities:
- Answer questions about Shopify orders, products, customers, and revenue
- Analyze trends and give actionable recommendations
- Help brainstorm product names, descriptions, and campaign ideas
- Explain ecommerce metrics in plain English
When the user asks a data question, the system will fetch the relevant Shopify data and include it
in your context. Use that data to give specific, accurate answers with real numbers.
Format guidelines:
- Use bullet points for lists
- Bold key numbers and product names
- Keep responses scannable — merchants are busy
- End data summaries with 1 actionable insight
You do NOT have access to external URLs, social media, or data outside of what's provided to you.`;
// Classify whether a question needs live data
async function detectDataIntent(question: string): Promise<{
needsData: boolean;
source?: string;
type?: string;
filters?: Record<string, unknown>;
}> {
const result = await reeve.ai.complete({
prompt: question,
systemPrompt: `You are a data intent classifier. Given a user question about their ecommerce store,
determine if it requires fetching live data.
Respond with ONLY valid JSON in this exact format:
{
"needsData": true/false,
"source": "shopify" | "email" | null,
"type": "orders" | "products" | "customers" | "revenue" | "campaigns" | null,
"filters": {}
}
Examples:
Q: "What were my top products last week?" → {"needsData":true,"source":"shopify","type":"products","filters":{"period":"7d"}}
Q: "How do I write a good product description?" → {"needsData":false,"source":null,"type":null,"filters":{}}
Q: "How many orders today?" → {"needsData":true,"source":"shopify","type":"orders","filters":{"period":"today"}}`,
intent: 'classification',
maxTokens: 100,
temperature: 0,
});
try {
return JSON.parse(result.content);
} catch {
return { needsData: false };
}
}
// Fetch Shopify data based on intent
async function fetchStoreData(intent: {
source: string;
type: string;
filters: Record<string, unknown>;
}) {
const { source, type, filters } = intent;
// Translate period filter to date range
const now = new Date();
const dateFilter: Record<string, string> = {};
if (filters.period === 'today') {
dateFilter.created_at_min = new Date(
now.getFullYear(), now.getMonth(), now.getDate()
).toISOString();
} else if (filters.period === '7d') {
const d = new Date(now);
d.setDate(d.getDate() - 7);
dateFilter.created_at_min = d.toISOString();
} else if (filters.period === '30d') {
const d = new Date(now);
d.setDate(d.getDate() - 30);
dateFilter.created_at_min = d.toISOString();
}
const result = await reeve.data.query({
source: source as 'shopify',
type,
filters: { ...dateFilter },
limit: 20,
});
return result.data;
}
// Main agent response function
export async function agentRespond(
history: Message[],
userMessage: string,
onChunk: (delta: string, done: boolean) => void
) {
// Check if we need data
const intent = await detectDataIntent(userMessage);
let dataContext = '';
if (intent.needsData && intent.source && intent.type) {
try {
const data = await fetchStoreData({
source: intent.source,
type: intent.type,
filters: intent.filters ?? {},
});
dataContext = `\n\n[LIVE STORE DATA — ${intent.type} from ${intent.source}]\n${JSON.stringify(data, null, 2)}\n[END DATA]`;
} catch (err) {
dataContext = `\n\n[NOTE: Could not fetch ${intent.type} data — ${intent.source} may not be connected]`;
}
}
// Build conversation history for the prompt
const conversationHistory = history
.slice(-10) // last 10 messages for context
.map((m) => `${m.role === 'user' ? 'User' : 'Store Concierge'}: ${m.content}`)
.join('\n\n');
const fullPrompt = [
conversationHistory,
`User: ${userMessage}${dataContext}`,
].filter(Boolean).join('\n\n');
await reeve.ai.stream(
{
prompt: fullPrompt,
systemPrompt: SYSTEM_PROMPT,
intent: 'chat',
maxTokens: 600,
temperature: 0.8,
},
(chunk) => {
onChunk(chunk.delta, chunk.done);
}
);
}4. Build the Chat UI
Replace src/App.tsx:
import { useEffect, useState, useRef, useCallback } from 'react';
import { reeve } from '@reeve/app-sdk';
import { agentRespond, type Message } from './agent';
import './App.css';
const QUICK_QUESTIONS = [
"What's my revenue today?",
"Show me my top 5 products this month",
"How many orders came in this week?",
"What's my average order value?",
"Which products need restocking?",
];
export default function App() {
const [ready, setReady] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{
id: 'welcome',
role: 'assistant',
content: "👋 Hey! I'm Store Concierge. Ask me anything about your store — revenue, orders, products, customers. What's on your mind?",
timestamp: new Date(),
},
]);
const [input, setInput] = useState('');
const [responding, setResponding] = useState(false);
const bottomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
reeve.init().then(() => setReady(true));
}, []);
// Scroll to bottom on new messages
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = useCallback(async (text: string) => {
if (!text.trim() || responding) return;
const userMsg: Message = {
id: `u-${Date.now()}`,
role: 'user',
content: text,
timestamp: new Date(),
};
const assistantMsg: Message = {
id: `a-${Date.now()}`,
role: 'assistant',
content: '',
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMsg, assistantMsg]);
setInput('');
setResponding(true);
try {
await agentRespond(
[...messages, userMsg],
text,
(delta, done) => {
setMessages((prev) =>
prev.map((m) =>
m.id === assistantMsg.id
? { ...m, content: m.content + delta }
: m
)
);
}
);
} catch (err) {
setMessages((prev) =>
prev.map((m) =>
m.id === assistantMsg.id
? { ...m, content: "Sorry, I ran into an issue. Please try again." }
: m
)
);
} finally {
setResponding(false);
}
}, [messages, responding]);
if (!ready) return <div className="loading">Starting Store Concierge...</div>;
return (
<div className="chat-app">
{/* Header */}
<div className="chat-header">
<div className="agent-avatar">🏪</div>
<div>
<div className="agent-name">Store Concierge</div>
<div className="agent-status">
{responding ? 'Thinking...' : 'Online'}
</div>
</div>
</div>
{/* Messages */}
<div className="messages-container">
{messages.map((msg) => (
<div key={msg.id} className={`message message-${msg.role}`}>
{msg.role === 'assistant' && (
<div className="message-avatar">🏪</div>
)}
<div className="message-bubble">
{msg.content || (responding && <span className="cursor-blink">▋</span>)}
</div>
</div>
))}
<div ref={bottomRef} />
</div>
{/* Quick questions (show only at start) */}
{messages.length <= 2 && (
<div className="quick-questions">
{QUICK_QUESTIONS.map((q) => (
<button key={q} className="quick-q" onClick={() => sendMessage(q)}>
{q}
</button>
))}
</div>
)}
{/* Input */}
<div className="chat-input-row">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(input);
}
}}
placeholder="Ask about your store..."
disabled={responding}
/>
<button
onClick={() => sendMessage(input)}
disabled={responding || !input.trim()}
>
Send
</button>
</div>
</div>
);
}5. Style the Chat Interface
Replace src/App.css:
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--reeve-font);
background: var(--reeve-bg);
color: var(--reeve-text);
height: 100vh;
display: flex;
flex-direction: column;
}
.chat-app {
display: flex;
flex-direction: column;
height: 100vh;
}
.chat-header {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: var(--reeve-surface);
border-bottom: 1px solid var(--reeve-border);
}
.agent-avatar {
font-size: 1.5rem;
width: 40px;
height: 40px;
background: var(--reeve-surface-2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.agent-name {
font-weight: 600;
font-size: 0.95rem;
}
.agent-status {
font-size: 0.75rem;
color: var(--reeve-success);
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
display: flex;
gap: 8px;
max-width: 85%;
}
.message-user {
align-self: flex-end;
flex-direction: row-reverse;
}
.message-avatar {
width: 28px;
height: 28px;
background: var(--reeve-surface-2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8rem;
flex-shrink: 0;
}
.message-bubble {
background: var(--reeve-surface);
border: 1px solid var(--reeve-border);
border-radius: var(--reeve-radius-lg);
padding: 10px 14px;
font-size: 0.875rem;
line-height: 1.6;
white-space: pre-wrap;
}
.message-user .message-bubble {
background: var(--reeve-accent);
color: white;
border-color: transparent;
}
.cursor-blink {
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% { opacity: 0; }
}
.quick-questions {
padding: 0 16px 12px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.quick-q {
background: var(--reeve-surface);
border: 1px solid var(--reeve-border);
border-radius: 20px;
padding: 6px 12px;
font-size: 0.75rem;
cursor: pointer;
color: var(--reeve-text);
transition: border-color 0.15s, background 0.15s;
}
.quick-q:hover {
border-color: var(--reeve-accent);
background: var(--reeve-surface-2);
}
.chat-input-row {
display: flex;
gap: 8px;
padding: 12px 16px;
background: var(--reeve-surface);
border-top: 1px solid var(--reeve-border);
}
input {
flex: 1;
background: var(--reeve-surface-2);
border: 1px solid var(--reeve-border);
border-radius: var(--reeve-radius);
padding: 10px 14px;
font-size: 0.875rem;
color: var(--reeve-text);
font-family: var(--reeve-font);
outline: none;
}
input:focus {
border-color: var(--reeve-accent);
}
input:disabled {
opacity: 0.6;
}
button {
background: var(--reeve-accent);
color: white;
border: none;
border-radius: var(--reeve-radius);
padding: 10px 18px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
white-space: nowrap;
}
button:hover:not(:disabled) {
background: var(--reeve-accent-hover);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
color: var(--reeve-text-muted);
}6. Test and Refine
npm run devInstall in the Cockpit and try some conversations:
- "What's my revenue this week?" — fetches Shopify data, gives a real number
- "Which products are selling fastest?" — top products query
- "How should I price my new product?" — no data needed, conversational
- "Are my orders up or down compared to last week?" — trend analysis
Tuning the system prompt: The personality is entirely in SYSTEM_PROMPT in src/agent.ts. Experiment with:
- More specific persona ("You specialize in sustainable fashion brands")
- Stricter formatting rules ("Always respond in bullet points")
- Domain knowledge ("The merchant sells B2B. Always refer to 'clients' not 'customers'")
7. List in the Marketplace
When you're ready to publish:
- Build your app:
npm run build - Deploy to a hosting provider (Vercel, Netlify, Cloudflare Pages)
- Update
reeve-manifest.jsonwith your production URL - Submit for review at meetreeve.com/developers/submit
See App Review Guidelines for what the review team checks.
Agent apps with a compelling, niche personality tend to perform best in the Marketplace. A generic "store assistant" competes with Reeve's built-in agent. A "Skincare brand concierge" or "B2B wholesale assistant" has a clear, differentiated value proposition.
What's Next
- Persistent memory — save conversation history to
reeve.storageso the agent remembers past chats - Multi-source data — connect email, ads, and analytics data alongside Shopify
- Proactive notifications — use
events.subscribeto alert when a big order comes in - Exportable reports — let users download a PDF summary of the conversation