Reeve
DevelopersTutorials

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 dev

The 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:

  1. Intent detection — classify whether the question needs live data
  2. 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 answer

For 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 dev

Install 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:

  1. Build your app: npm run build
  2. Deploy to a hosting provider (Vercel, Netlify, Cloudflare Pages)
  3. Update reeve-manifest.json with your production URL
  4. 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.storage so the agent remembers past chats
  • Multi-source data — connect email, ads, and analytics data alongside Shopify
  • Proactive notifications — use events.subscribe to alert when a big order comes in
  • Exportable reports — let users download a PDF summary of the conversation

On this page