commune-ai
v0.2.2
Published
Our email infrastructure - webhooks, threads, history, and semantic search
Readme
@commune/sdk
Commune is the communication infrastructure for agents. It gives your agent a unified inbox for email + Slack, so your agent can talk to humans where they already work. Most teams get a working integration in ~15 minutes.
Why Commune exists (what it enables)
Agents are powerful, but users already live in email and Slack. Commune bridges that gap so:
- your agent is reachable where humans already work
- you don’t have to build deliverability, threading, or Slack plumbing
- you can ship an agent‑first experience in minutes, not weeks
In practice, Commune lets you:
- give an agent a real inbox on your domain
- respond in the correct email or Slack thread every time
- use conversation state to make smarter, context‑aware replies
How it works (mental model)
- Commune receives inbound email/Slack events.
- Commune normalizes them into a UnifiedMessage.
- Commune sends the UnifiedMessage to your webhook.
- Your agent replies using one API call.
By default, the SDK talks to the hosted Commune API. If you self‑host,\n> pass
baseUrlto the client.
Quickstart (end‑to‑end in one file)
This is the simplest full flow: receive webhook → run agent → reply in thread.
import express from "express";
import { CommuneClient, createWebhookHandler, verifyCommuneWebhook } from "@commune/sdk";
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
const handler = createWebhookHandler({
verify: ({ rawBody, headers }) => {
const signature = headers["x-commune-signature"];
const timestamp = headers["x-commune-timestamp"];
if (!signature || !timestamp) return false;
return verifyCommuneWebhook({
rawBody,
timestamp,
signature,
secret: process.env.COMMUNE_WEBHOOK_SECRET!,
});
},
onEvent: async (message, context) => {
// Example inbound payload (unified across email + Slack):
// message = {
// channel: "email",
// conversation_id: "thread_id",
// participants: [{ role: "sender", identity: "[email protected]" }],
// content: "Can you help with pricing?"
// }
// --- Run your agent here (1–2 line LLM call) ---
const prompt = `Reply to: ${message.content}`;
const agentReply = await llm.complete(prompt); // replace with your LLM client
// Email reply (same thread)
if (message.channel === "email") {
const sender = message.participants.find(p => p.role === "sender")?.identity;
if (!sender) return;
await client.messages.send({
channel: "email",
to: sender,
text: agentReply,
conversation_id: message.conversation_id,
domainId: context.payload.domainId,
inboxId: context.payload.inboxId,
});
}
// Slack reply (same thread)
if (message.channel === "slack") {
const channelId = message.metadata.slack_channel_id;
if (!channelId) return;
await client.messages.send({
channel: "slack",
to: channelId,
text: agentReply,
conversation_id: message.conversation_id, // thread_ts
});
}
},
});
const app = express();
app.post("/commune/webhook", express.raw({ type: "*/*" }), handler);
app.listen(3000, () => console.log("listening on 3000"));0) Install
npm install @commune/sdkUnified inbox (what your webhook receives)
Every inbound email or Slack message arrives in this shape:
export interface UnifiedMessage {
channel: "email" | "slack";
message_id: string;
conversation_id: string; // email thread or Slack thread_ts
participants: { role: string; identity: string }[];
content: string;
metadata: { slack_channel_id?: string; ... };
}API key (required)
All /api/* requests require an API key. Create one in the dashboard and reuse it in your client.
export COMMUNE_API_KEY="your_key_from_dashboard"
export COMMUNE_WEBHOOK_SECRET="your_webhook_secret"const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });Context (conversation state)
Commune stores conversation state so your agent can respond with context.
import { CommuneClient } from "@commune/sdk";
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
// Thread history (email thread or Slack thread)
const thread = await client.messages.listByConversation(message.conversation_id, {
order: "asc",
limit: 50,
});
// All messages from a user
const userHistory = await client.messages.list({
sender: "[email protected]",
limit: 25,
});
// All messages in a specific inbox
const inboxMessages = await client.messages.list({
inbox_id: "i_xxx",
channel: "email",
limit: 50,
});Cross‑channel behavior (email + Slack in one handler)
The same UnifiedMessage shape works for both channels. You only branch on channel.
import express from "express";
import { CommuneClient, createWebhookHandler } from "@commune/sdk";
// Hosted API is default. If self-hosted, pass { baseUrl: "https://your-api" }
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
const handler = createWebhookHandler({
onEvent: async (message, context) => {
if (message.channel === "email") {
const sender = message.participants.find(p => p.role === "sender")?.identity;
if (!sender) return;
await client.messages.send({
channel: "email",
to: sender,
text: "Got it — thanks for the message.",
conversation_id: message.conversation_id,
domainId: context.payload.domainId,
inboxId: context.payload.inboxId,
});
}
if (message.channel === "slack") {
const channelId = message.metadata.slack_channel_id;
if (!channelId) return;
await client.messages.send({
channel: "slack",
to: channelId,
text: "Replying in thread ✅",
conversation_id: message.conversation_id,
});
}
},
});
const app = express();
app.post("/commune/webhook", express.raw({ type: "*/*" }), handler);
app.listen(3000);Setup instructions (dashboard-first)
Domain setup and inbox creation are done in the Commune dashboard. You then copy the IDs into your code.
1) Create a subdomain for the agent
Use a subdomain like agents.yourcompany.com for deliverability and isolation.
2) Create and verify the domain in the dashboard
The dashboard guides you through DNS (SPF/DKIM/MX) and verification.
3) Create inboxes for your agents in the dashboard
Each inbox represents an agent address (e.g. [email protected]).
4) Use IDs from the webhook payload
The webhook payload already includes:
domainId(e.g.d_xxx)inboxId(e.g.i_xxx)
Use them when replying:
await client.messages.send({
channel: "email",
to: "[email protected]",
text: "Thanks — replying in thread.",
conversation_id: message.conversation_id,
domainId: context.payload.domainId,
inboxId: context.payload.inboxId,
});The SDK also supports programmatic domain/inbox creation, but the dashboard flow is the primary path for most teams.
5) Set your webhook secret
When you configure the inbox webhook in the dashboard, Commune shows a webhook secret. Store it as:
export COMMUNE_WEBHOOK_SECRET="your_webhook_secret"Use it in the verify function shown above.
6) Create an API key in the dashboard
Use the dashboard to create an API key, then set it as:
export COMMUNE_API_KEY="your_key_from_dashboard"Webhook verification (Commune → your app)
Commune signs outbound webhooks using your inbox webhook secret. Verify the signature before processing the request.
import { createWebhookHandler, verifyCommuneWebhook } from "@commune/sdk";
const handler = createWebhookHandler({
verify: ({ rawBody, headers }) => {
const signature = headers["x-commune-signature"];
const timestamp = headers["x-commune-timestamp"];
if (!signature || !timestamp) return false;
return verifyCommuneWebhook({
rawBody,
timestamp,
signature,
secret: process.env.COMMUNE_WEBHOOK_SECRET!,
});
},
onEvent: async (message) => {
// handle verified message
},
});Full example (single file)
A complete copy‑paste example that:
- receives webhook
- replies by email
- replies in Slack thread
- fetches conversation history
import express from "express";
import { CommuneClient, createWebhookHandler } from "@commune/sdk";
const client = new CommuneClient({ apiKey: process.env.COMMUNE_API_KEY! });
const handler = createWebhookHandler({
onEvent: async (message, context) => {
// 1) Email reply (same thread)
if (message.channel === "email") {
const sender = message.participants.find(p => p.role === "sender")?.identity;
if (!sender) return;
await client.messages.send({
channel: "email",
to: sender,
text: "Thanks! We received your email.",
conversation_id: message.conversation_id,
domainId: context.payload.domainId,
inboxId: context.payload.inboxId,
});
return;
}
// 2) Slack reply (same thread)
if (message.channel === "slack") {
const channelId = message.metadata.slack_channel_id;
if (!channelId) return;
await client.messages.send({
channel: "slack",
to: channelId,
text: "Thanks! Replying in thread.",
conversation_id: message.conversation_id,
});
}
},
});
const app = express();
app.post("/commune/webhook", express.raw({ type: "*/*" }), handler);
app.listen(3000, () => console.log("listening on 3000"));