Build a Chat App
Composio gives your AI agent access to 800+ app integrations — Gmail, GitHub, Slack, Notion, and more. Today, in about 30 lines of backend code, we'll build a chat app where your agent can discover tools, authenticate users, and take actions across any app.
After that we'll see how authentication works and how to show tool calls in the UI.
Create the project
Create a new Next.js app and install the dependencies:
pnpm create next-app composio-chat --yes
cd composio-chat
pnpm install @composio/core @composio/vercel @ai-sdk/openai ai @ai-sdk/reactAdd your API keys to a .env.local file in the project root:
COMPOSIO_API_KEY=your_composio_api_key
OPENAI_API_KEY=your_openai_api_keyStart the dev server and keep it running throughout this tutorial:
pnpm devOpen localhost:3000 — you'll see the default Next.js page. We'll replace it with a chat interface shortly.
How Composio works
Instead of loading 1000+ tools into your agent's context (which would overwhelm any LLM), Composio gives your agent 5 meta tools that handle everything at runtime:
| Meta tool | What it does |
|---|---|
COMPOSIO_SEARCH_TOOLS | Discover relevant tools across 800+ apps |
COMPOSIO_MANAGE_CONNECTIONS | Handle OAuth and API key authentication |
COMPOSIO_MULTI_EXECUTE_TOOL | Execute up to 20 tools in parallel |
COMPOSIO_REMOTE_WORKBENCH | Run Python code in a persistent sandbox |
COMPOSIO_REMOTE_BASH_TOOL | Execute bash commands for data processing |
Here's the flow:
User: "Star the composio repo on GitHub"
↓
1. Agent calls COMPOSIO_SEARCH_TOOLS
→ Returns GITHUB_STAR_REPO with input schema
→ Returns connection status (not connected)
↓
2. Agent calls COMPOSIO_MANAGE_CONNECTIONS
→ Returns an auth link for GitHub
→ User clicks link and authorizes
↓
3. Agent calls COMPOSIO_MULTI_EXECUTE_TOOL
→ Executes GITHUB_STAR_REPO
→ Returns success
↓
Done. The repo is starred. ⭐When you create a session, your agent gets these 5 tools. From there, the agent handles discovery, authentication, and execution on its own.
Now let's write the code.
Your first session
A session is the core Composio primitive. It scopes tools and connections to a specific user. One line of code gives your agent access to everything.
Create a new file at app/api/chat/route.ts:
import { openai } from "@ai-sdk/openai";
import { Composio } from "@composio/core";
import { VercelProvider } from "@composio/vercel";
import {
streamText,
convertToModelMessages,
generateId,
stepCountIs,
type UIMessage,
} from "ai";
const composio = new Composio({ provider: new VercelProvider() });
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
// Create a session for the user
const session = await composio.create("user_123");
const tools = await session.tools();
// Stream the AI response with tool access
const result = streamText({
model: openai("gpt-4o"),
system: "You are a helpful assistant. Use Composio tools to help the user.",
messages: await convertToModelMessages(messages),
tools,
stopWhen: stepCountIs(10),
});
return result.toUIMessageStreamResponse({
originalMessages: messages,
generateMessageId: () => generateId(),
});
}Let's break this down:
new Composio({ provider: new VercelProvider() })initializes Composio with the Vercel AI SDK provider, so tools are returned in the right format.composio.create("user_123")creates a session for this user. All tool connections and data are scoped to this ID. In production, you'd use a real user ID from your auth system.session.tools()returns the 5 meta tools, formatted for the Vercel AI SDK.streamTextsends the conversation to GPT-4o along with the tools. The model can now search for tools, authenticate users, and execute actions — all through those meta tools.stopWhen: stepCountIs(10)limits the agent to 10 steps per request. Each tool call counts as a step.
That's the entire backend. Now let's build the chat UI.
The chat UI
Replace your app/page.tsx with a simple chat interface:
"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
export default function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
const isLoading = status === "streaming" || status === "submitted";
return (
<main className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Composio Chat</h1>
<div className="space-y-4 mb-6 min-h-[200px]">
{messages.length === 0 && (
<p className="text-gray-400 text-center py-12">
Try: "Star the composio repo on GitHub"
</p>
)}
{messages.map((m) => (
<div key={m.id} className="flex gap-2">
<span className="font-semibold shrink-0">
{m.role === "user" ? "You:" : "Agent:"}
</span>
<div>
{m.parts.map((part, i) =>
part.type === "text" ? <span key={i}>{String(part.text)}</span> : null
)}
</div>
</div>
))}
{isLoading && (
<p className="text-gray-400 text-sm">Thinking...</p>
)}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ text: input });
setInput("");
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask me anything..."
disabled={isLoading}
className="w-full p-3 border border-gray-300 rounded-lg"
/>
</form>
</main>
);
}The useChat hook manages everything: message history, streaming, and state. It sends messages to your /api/chat route and streams the response back automatically.
Go to localhost:3000. You should see the chat interface. Try sending a message like "Hello" — the agent will respond.
Your first tool call
Now try asking the agent to do something real:
Star the composio repo on GitHubHere's what happens behind the scenes:
-
The agent calls
COMPOSIO_SEARCH_TOOLSwith your request. Composio returns theGITHUB_STAR_REPOtool along with its input schema and the user's connection status. -
Since you haven't connected GitHub yet, the agent calls
COMPOSIO_MANAGE_CONNECTIONS. Composio returns an authentication link. -
The agent shows you the link in the chat. Click it, authorize with GitHub, and come back to the chat.
-
Tell the agent you're done (e.g., "Done" or "I've connected"). It retries — this time calling
COMPOSIO_MULTI_EXECUTE_TOOLto runGITHUB_STAR_REPO. -
The repo is starred.
The agent handled discovery, authentication, and execution — all through the meta tools you gave it with composio.create(). You didn't write a single line of tool-specific code.
This in-chat authentication flow works out of the box. For production apps, you can also authenticate users ahead of time during onboarding.
Showing tool calls
Right now tool calls happen invisibly. Let's add a component that shows them — the tool name, input, and output — so you can see what your agent is doing in real time.
Create components/ToolCallDisplay.tsx:
"use client";
import { useState } from "react";
export function ToolCallDisplay({
toolName,
input,
output,
isLoading,
}: {
toolName: string;
input: unknown;
output?: unknown;
isLoading: boolean;
}) {
const [expanded, setExpanded] = useState(false);
return (
<div
className={`my-2 rounded-lg border p-2 text-sm ${
isLoading
? "border-blue-200 bg-blue-50"
: "border-green-200 bg-green-50"
}`}
>
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 w-full text-left"
>
<span>{isLoading ? "⏳" : "✅"}</span>
<code className="font-mono text-xs">{toolName}</code>
{isLoading && <span className="text-xs text-gray-500">Running...</span>}
<span className="ml-auto text-xs">{expanded ? "▲" : "▼"}</span>
</button>
{expanded && (
<div className="mt-2 space-y-2">
<pre className="text-xs bg-white/50 p-2 rounded overflow-auto max-h-32">
{String(JSON.stringify(input, null, 2))}
</pre>
{output != null && (
<pre className="text-xs bg-white/50 p-2 rounded overflow-auto max-h-32">
{String(JSON.stringify(output, null, 2))}
</pre>
)}
</div>
)}
</div>
);
}Now update app/page.tsx to render tool calls:
"use client";
import { useState } from "react";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport, getToolName, isToolUIPart } from "ai";
import { ToolCallDisplay } from "../components/ToolCallDisplay";
export default function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
const isLoading = status === "streaming" || status === "submitted";
return (
<main className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">Composio Chat</h1>
<div className="space-y-4 mb-6 min-h-[200px]">
{messages.length === 0 && (
<p className="text-gray-400 text-center py-12">
Try: "Star the composio repo on GitHub"
</p>
)}
{messages.map((m) => (
<div key={m.id} className="flex gap-2">
<span className="font-semibold shrink-0">
{m.role === "user" ? "You:" : "Agent:"}
</span>
<div className="flex-1">
{m.parts.map((part, i) => {
if (part.type === "text") {
return <span key={i}>{String(part.text)}</span>;
}
if (isToolUIPart(part)) {
return (
<ToolCallDisplay
key={part.toolCallId}
toolName={getToolName(part)}
input={part.input}
output={
part.state === "output-available"
? part.output
: undefined
}
isLoading={part.state !== "output-available"}
/>
);
}
return null;
})}
</div>
</div>
))}
{isLoading && (
<p className="text-gray-400 text-sm">Thinking...</p>
)}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ text: input });
setInput("");
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask me anything..."
disabled={isLoading}
className="w-full p-3 border border-gray-300 rounded-lg"
/>
</form>
</main>
);
}Now when the agent uses tools, you'll see expandable cards showing what's happening — the tool name while it's running, and the full input/output when you click to expand.
Try sending a few more requests:
- "Summarize my emails from today"
- "What's on my calendar this week?"
- "Create a GitHub issue in my repo"
Each one will trigger a different set of tools, and you'll see the full flow: search → authenticate → execute.
What you built
With about 30 lines of backend code and a simple React frontend, you've built a chat app that:
- Creates a session that gives the agent 5 meta tools
- Uses
COMPOSIO_SEARCH_TOOLSto discover the right tools across 800+ apps - Uses
COMPOSIO_MANAGE_CONNECTIONSto handle OAuth inline - Uses
COMPOSIO_MULTI_EXECUTE_TOOLto take actions on the user's behalf - Streams responses and shows tool calls in real time
The key concept is the session — one line (composio.create("user_123")) gives your agent access to every integration Composio supports.
Next up
We just touched on the basics. Here are some good next steps:
Configuring sessions
Restrict toolkits, set auth configs, and customize session behavior
Manual authentication
Authenticate users during onboarding instead of in-chat
Users & Sessions
Understand the user model for multi-user production apps
Full Stack Chat App
A production chat app with connection management UI