# Reeve Documentation (/docs)
What is Reeve? [#what-is-reeve]
Reeve is an **AI business operating system**. It connects to the tools you already use β Shopify, Stripe, Meta Ads, Google Analytics, Klaviyo, Slack β and puts an AI in front of all of it.
Instead of logging into six dashboards, you ask Reeve:
> *"How did we do last week?"*
> *"Which ads should I pause?"*
> *"Draft an email campaign for our spring sale."*
Reeve pulls live data from your connected platforms, gives you clear answers, and takes action β all through conversation.
Get Started [#get-started]
Download the app and get Reeve running in 5 minutes
Set up Shopify, Stripe, Meta Ads, and more
See all your business metrics in one place
Sign up, invite your team, and manage API keys
**Power user or self-hoster?** Reeve is open source and runs on your own infrastructure. See the [CLI setup guide](/docs/getting-started/cli-setup) for the terminal-based setup, or the [Cloud setup guide](/docs/getting-started/cloud-setup) if you want zero installation.
What Reeve Can Do [#what-reeve-can-do]
Unified revenue, traffic, and conversion metrics across all platforms
Monitor Meta and Google campaigns, generate creatives, analyze competitors
Create and send Klaviyo campaigns from a chat prompt
Stripe MRR, LTV, churn, and subscription analytics
Track followers, engagement, and top-performing posts
Competitive analysis, brand profiling, ad library research
Score candidates, generate JDs, manage your hiring pipeline
Zendesk and Gorgias integration β ticket overview, CSAT, SLA tracking
Products, orders, customers, and store health
Monitor parallel agent sessions in real time
How It Works [#how-it-works]
1. **Connect your tools** β OAuth or API key setup takes under a minute per platform
2. **Ask questions** β Chat with Reeve about your business in plain language
3. **Get answers** β AI-powered analysis across all connected platforms
4. **Take action** β Create campaigns, generate ads, score candidates, and more
Reeve is available as a [Desktop App](/docs/desktop) for macOS, as a [web app](https://app.meetreeve.com), and through messaging apps like [Slack](/docs/connectors/slack), [WhatsApp](/docs/channels/whatsapp), and [Telegram](/docs/channels/telegram).
Explore the Docs [#explore-the-docs]
Understand agents, sessions, memory, and the gateway
Every command, flag, and option
Layers, examples, and API reference
Multi-phase goals with budgets, triggers, and service loops
Browser, exec, subagents, and custom skills
WhatsApp, Telegram, Discord, Slack, and more
Plans [#plans]
| Plan | Price | What's included |
| --------- | ------- | ---------------------------------------------------- |
| **Free** | $0/mo | 2 connected platforms, basic analytics, chat |
| **Pro** | $40/mo | Unlimited connectors, ads, email, recruiter, support |
| **Team** | $120/mo | Multi-user access, shared dashboards, team roles |
| **Cloud** | Custom | Managed hosting, SLA, dedicated support |
Full details at [meetreeve.com/pricing](https://meetreeve.com/pricing).
For LLMs & AI Agents [#for-llms--ai-agents]
Reeve's docs are optimized for LLM consumption:
* **[llms.txt](/llms.txt)** β Page index with titles, URLs, and descriptions
* **[llms-full.txt](/llms-full.txt)** β All documentation in a single markdown file
* **Per-page markdown** β Append `.mdx` to any docs URL path prefix (e.g., `/llms.mdx/docs/agents`) for raw markdown
* **Copy button** β Every page has a "Copy as Markdown" button for easy paste into any AI chat
# Connectors API (/docs/api/connectors)
Connectors API [#connectors-api]
The Connectors API manages third-party integrations β handling OAuth authorization flows, API key storage, token refresh, and connection lifecycle.
Authentication Methods [#authentication-methods]
Reeve uses two methods depending on the platform:
| Method | Platforms | How It Works |
| ------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- |
| **OAuth 2.0** | Meta Ads, Google Ads, TikTok Ads, Shopify, GitHub, Slack | Click Connect β authorize on the platform β redirected back |
| **API Key** | Stripe, PostHog, Klaviyo, GA4, Gorgias, Zendesk, Vercel, Railway | Paste your key β validated and encrypted |
Available Connectors [#available-connectors]
Commerce & Payments [#commerce--payments]
| Connector | Auth | Tools Enabled |
| ----------------------------------- | ------- | ------------------------------------------------------ |
| [Shopify](/docs/connectors/shopify) | OAuth | `reeve_shopify` β products, orders, customers, revenue |
| Stripe | API Key | `reeve_revenue` β MRR, LTV, churn, subscriptions |
Advertising [#advertising]
| Connector | Auth | Tools Enabled |
| ----------------------------------------- | ----- | --------------------------------------------------- |
| [Meta Ads](/docs/connectors/meta-ads) | OAuth | `reeve_ads` β campaigns, ROAS, creative performance |
| [Google Ads](/docs/connectors/google-ads) | OAuth | `reeve_ads` β search/display campaigns, keywords |
| TikTok Ads | OAuth | `reeve_ads` β short-form video campaign data |
Marketing [#marketing]
| Connector | Auth | Tools Enabled |
| ----------------------------------- | ------- | ----------------------------------------------- |
| [Klaviyo](/docs/connectors/klaviyo) | API Key | `reeve_email` β campaigns, flows, segments, SMS |
Analytics [#analytics]
| Connector | Auth | Tools Enabled |
| ---------------- | --------------- | ---------------------------------------------------- |
| PostHog | API Key | `reeve_analytics` β funnels, sessions, feature flags |
| Google Analytics | Service Account | `reeve_analytics` β traffic, top pages, conversions |
Customer Support [#customer-support]
| Connector | Auth | Tools Enabled |
| --------- | ------- | ----------------------------------------------- |
| Gorgias | API Key | `reeve_support` β tickets, response times, CSAT |
| Zendesk | API Key | `reeve_support` β tickets, agent performance |
Communication [#communication]
| Connector | Auth | Tools Enabled |
| ------------------------------- | ----- | ----------------------------- |
| [Slack](/docs/connectors/slack) | OAuth | Channel messaging, bot access |
Development [#development]
| Connector | Auth | Tools Enabled |
| --------- | --------- | -------------------------------------- |
| GitHub | OAuth/App | Repos, PRs, CI/CD, deployments |
| Vercel | API Key | Deployment status, domains, build logs |
| Railway | API Key | Service status, deployment history |
| Figma | API Key | Design files, components |
OAuth Flow [#oauth-flow]
Step 1: Initiate [#step-1-initiate]
The Cockpit sends a request to start the OAuth flow:
```
GET /api/oauth/{provider}/authorize
Authorization: Bearer
```
Returns an authorization URL that redirects the user to the platform's consent screen.
Step 2: User Authorizes [#step-2-user-authorizes]
The user reviews permissions and clicks Approve on the platform. The platform redirects back with an authorization code.
Step 3: Token Exchange [#step-3-token-exchange]
Reeve exchanges the authorization code for access and refresh tokens:
```
GET /api/oauth/{provider}/callback?code=abc123&state=xyz
```
Tokens are encrypted and stored. The connection is now active.
Step 4: Automatic Refresh [#step-4-automatic-refresh]
OAuth tokens are refreshed automatically before they expire. Meta tokens last \~60 days; Google tokens refresh indefinitely as long as the refresh token is valid.
Security [#security]
* **State parameter** β CSRF protection with 10-minute TTL on state tokens
* **HMAC verification** β Shopify callbacks include HMAC signature verification
* **Encrypted storage** β Tokens encrypted at rest in PostgreSQL
API Key Flow [#api-key-flow]
Connect [#connect]
```
POST /api/connectors/connect
Authorization: Bearer
{
"provider": "stripe",
"api_key": "sk_live_..."
}
```
The key is validated against the provider's API before storage. Invalid keys return an immediate error.
List Connections [#list-connections]
```
GET /api/connectors/list
Authorization: Bearer
```
Returns all connected platforms with status:
```json
{
"connections": [
{ "provider": "meta", "type": "oauth", "status": "connected" },
{ "provider": "stripe", "type": "api_key", "status": "connected" },
{ "provider": "shopify", "type": "oauth", "status": "disconnected" }
]
}
```
Disconnect [#disconnect]
```
DELETE /api/connectors/{provider}
Authorization: Bearer
```
Deletes stored tokens and revokes OAuth access where the provider supports it.
Webhooks [#webhooks]
Some connectors support inbound webhooks for real-time updates:
| Provider | Webhook Events |
| ----------- | -------------------------------------------------------- |
| **Shopify** | Order created, product updated, inventory changed |
| **Stripe** | Payment succeeded, subscription updated, invoice created |
| **GitHub** | Push events, PR opened, deployment status |
Webhook URLs are automatically configured during connector setup. Events are routed to your agent as system events.
Multi-Brand Support [#multi-brand-support]
In [multi-brand](/docs/features/multi-brand) mode, connectors are scoped per brand. Each brand can have its own Shopify store, ad accounts, and analytics β all managed from a single Reeve account.
Connector credentials are scoped per user. Each team member connects their own accounts. The dashboard aggregates data across all connected accounts for the team.
# API Overview (/docs/api/overview)
API Overview [#api-overview]
The Reeve Services API is the backend that powers connectors, analytics, billing, and brand intelligence. It sits between the Reeve gateway (where agents run) and external platforms (Shopify, Stripe, Meta, etc.).
What the API Does [#what-the-api-does]
| Domain | Purpose |
| ---------------------- | --------------------------------------------------------- |
| **Connectors** | OAuth flows and API key management for external platforms |
| **Dashboard** | Aggregated analytics across all connected sources |
| **Brand Intelligence** | Brand analysis, competitor monitoring, ad intelligence |
| **Credits & Billing** | Credit balance, purchases, tier metering |
| **Auth** | Session management and authentication |
| **Social** | Social account management and trend research |
| **Apps** | App Store registry for Reeve extensions |
Architecture [#architecture]
```
Cockpit (Frontend)
β
βββ /api/connectors/* βββΆ Services API
βββ /api/brand/* βββΆ β
βββ /api/dashboard/* βββΆ βββ PostgreSQL
βββ /api/credits/* βββΆ βββ External APIs
βββ /api/auth/* βββΆ βββ Stripe
Gateway (Agent Runtime)
β
βββ /api/credits/deduct βββΆ Services API
```
Authentication [#authentication]
The API uses two authentication methods depending on who's calling:
User Requests (Frontend β API) [#user-requests-frontend--api]
All user-facing requests use a session token from the Cockpit:
```
Authorization: Bearer
```
Session tokens are managed by Clerk and are included automatically when you use the Cockpit. They expire and refresh transparently.
Service Requests (Gateway β API) [#service-requests-gateway--api]
The gateway communicates with the Services API using a shared service token:
```
X-Reeve-Services-Token:
```
This is used for operations like credit deductions during agent sessions. You don't need to configure this manually β it's set up during deployment.
Base URL [#base-url]
| Environment | URL |
| -------------- | -------------------------------- |
| **Production** | `https://services.meetreeve.com` |
All endpoints are prefixed with `/api/`. For example, the connectors list endpoint is `GET /api/connectors/list`.
Rate Limits [#rate-limits]
The API applies rate limiting per user to prevent abuse:
| Tier | Requests/Minute | Notes |
| ------------ | --------------- | ------------------------------ |
| **Standard** | 60 | Most API endpoints |
| **Auth** | 10 | Login and token endpoints |
| **Heavy** | 20 | Brand analysis, trend research |
Rate-limited responses return `429 Too Many Requests` with a `Retry-After` header.
How Agents Use the API [#how-agents-use-the-api]
Agents don't call the Services API directly. Instead, they use specialized tools that resolve credentials through the gateway:
```typescript
// Agent uses a tool β gateway resolves credentials β calls Services API
reeve_shopify({ action: "get_overview" })
reeve_ads({ action: "get_performance", platform: "meta" })
reeve_email({ action: "list_campaigns" })
reeve_analytics({ action: "get_summary", period: "30d" })
```
The gateway handles token resolution, refresh, and error handling. Agents never see raw API keys or OAuth tokens.
Error Handling [#error-handling]
The API returns standard HTTP status codes:
| Code | Meaning |
| ----- | --------------------------------------- |
| `200` | Success |
| `400` | Bad request β check parameters |
| `401` | Unauthorized β invalid or expired token |
| `403` | Forbidden β insufficient permissions |
| `404` | Not found β resource doesn't exist |
| `429` | Rate limited β wait and retry |
| `500` | Internal error β contact support |
Error responses include a JSON body with details:
```json
{
"error": "connector_not_found",
"message": "No Shopify connector found for this account",
"status": 404
}
```
Data Privacy [#data-privacy]
* **Live fetching** β Data is fetched from external APIs in real time, not cached or stored
* **Encrypted credentials** β OAuth tokens and API keys are encrypted at rest
* **Scoped access** β Each user's connectors are isolated; no cross-account data access
* **Token refresh** β OAuth tokens are refreshed automatically before expiry
The Services API is an internal backend that powers the Cockpit and agent tools. You don't need to call it directly β use the [Cockpit](/docs/cockpit/overview) for visual access or agent tools for programmatic access.
OAuth flows, API keys, and webhook setup
Full technical reference for the Services API
# API Keys (/docs/auth/api-keys)
API Keys [#api-keys]
Reeve needs access to at least one LLM provider to power your agents. You can use your own API keys (BYOK) or managed models through Reeve Cloud.
BYOK vs Managed [#byok-vs-managed]
| Feature | BYOK (Bring Your Own Key) | Managed |
| ------------ | ------------------------------------------- | ---------------------------------------- |
| Setup | Add your API key from the provider | Included with Reeve Cloud subscription |
| Billing | Billed directly by the provider | Deducted from Reeve credit balance |
| Control | Full control over model selection and spend | Simplified β no external accounts needed |
| Availability | All models from all providers | Curated selection |
You can mix both β use managed models for convenience and BYOK for specific providers.
Adding API Keys [#adding-api-keys]
Navigate to **Settings β Models** in the Cockpit
Click **Add Provider** and select the provider
Paste your API key
Click **Test** β Reeve makes a real API call to verify the key works
Click **Save**
The UI shows a green indicator for connected providers and lets you drag-reorder the fallback chain.
Interactive setup:
```bash
reeve configure --section auth
```
Or via the onboarding wizard:
```bash
reeve onboard
```
Both walk you through adding provider keys interactively.
Edit `reeve.json` directly:
```json
{
"models": {
"providers": {
"anthropic": {
"apiKey": "sk-ant-api03-..."
},
"openai": {
"apiKey": "sk-..."
},
"openrouter": {
"apiKey": "sk-or-..."
}
}
}
}
```
Supported Providers [#supported-providers]
| Provider | Key prefix | Where to get it |
| ------------------- | --------------- | --------------------------------------------------------- |
| **Anthropic** | `sk-ant-` | [console.anthropic.com](https://console.anthropic.com) |
| **OpenAI** | `sk-` | [platform.openai.com](https://platform.openai.com) |
| **OpenRouter** | `sk-or-` | [openrouter.ai/keys](https://openrouter.ai/keys) |
| **Google (Gemini)** | `AI...` | [aistudio.google.com](https://aistudio.google.com/apikey) |
| **Amazon Bedrock** | IAM credentials | [AWS Console](https://console.aws.amazon.com) |
| **Ollama** | None (local) | [ollama.com](https://ollama.com) |
See [Providers](/docs/providers) for the complete list and model-specific configuration.
Fallback Chain [#fallback-chain]
When your primary provider fails (rate limit, outage, network error), Reeve automatically tries the next provider in the chain:
```json
{
"models": {
"fallback": ["anthropic", "openai", "openrouter"]
}
}
```
In this example, if Anthropic fails, Reeve tries OpenAI. If OpenAI also fails, it tries OpenRouter. Configure the order in the Cockpit via drag-and-drop, or in the config file.
See [Model Failover](/docs/concepts/model-failover) for details on how failover works.
Auth Profiles [#auth-profiles]
API keys are stored in **auth profiles** β encrypted files in the agent workspace:
```
~/.reeve/agents//agent/auth-profiles.json
```
Auth profiles support multiple key sources:
* Direct API keys (most common)
* OAuth credentials (for providers that support it)
* Environment variables (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`)
Reeve resolves keys in this order:
1. Auth profile for the specific agent
2. Global config (`models.providers.*.apiKey`)
3. Environment variables
Key Security [#key-security]
* API keys are stored encrypted via Reeve's TokenVault
* Keys are never exposed to agents β agents call tools, tools use keys internally
* Keys are never logged or included in error messages
* When displayed in the UI, keys are masked (e.g., `sk-ant-...****`)
* You can test keys before saving to avoid storing invalid credentials
Never share your API keys or commit them to version control. Use environment variables or the Cockpit UI for key management. Reeve encrypts stored keys, but the encryption is only as strong as your filesystem security.
Environment Variables [#environment-variables]
For server deployments, you can set keys via environment variables instead of config files:
```bash
export ANTHROPIC_API_KEY="sk-ant-..."
export OPENAI_API_KEY="sk-..."
export BRAVE_API_KEY="BSA..."
```
These are picked up automatically by the gateway on startup.
# Authentication (/docs/auth)
Authentication [#authentication]
Reeve uses a layered authentication system. Users sign in through a secure identity provider, APIs use session tokens, and team members join through invites.
Signing Up [#signing-up]
Create a Reeve account at [meetreeve.com](https://meetreeve.com):
1. Click **Get Started**
2. Sign up with **Google**, a **magic link** (email), or **email + password**
3. You're taken to the [Cockpit](/docs/cockpit/overview) β ready to go
No credit card required for the Free tier.
Signing In [#signing-in]
Reeve supports three sign-in methods:
| Method | How it works |
| -------------------- | ----------------------------------------------------------- |
| **Google OAuth** | One click β sign in with your Google account |
| **Magic link** | Enter your email β click the link in your inbox β signed in |
| **Email + password** | Traditional credentials |
Desktop App [#desktop-app]
The Desktop App handles authentication automatically:
1. Launch Reeve from Applications
2. A browser window opens for sign-in
3. Authenticate with any method above
4. The app receives a session token and you're connected
You only need to sign in once β the session token is stored locally and refreshes automatically.
Cloud (Browser) [#cloud-browser]
Go to [app.meetreeve.com](https://app.meetreeve.com) and sign in. Your session persists across browser tabs and restarts.
Team Access [#team-access]
Inviting Team Members [#inviting-team-members]
On **Team** ($120/mo) and **Cloud** plans, you can invite team members:
1. Go to **Settings** in the Cockpit
2. Click **Team** β **Invite Member**
3. Enter their email address
4. They receive an invitation email with a link to join
Team members get their own login and can access shared connectors, dashboards, and agents based on their role.
Roles [#roles]
| Role | What they can do |
| ---------- | ---------------------------------------------------- |
| **Owner** | Full access β billing, team management, all features |
| **Admin** | Manage connectors, agents, and settings (no billing) |
| **Member** | View dashboards, chat with agents, view data |
Session Tokens [#session-tokens]
After authentication, Reeve issues a **session token** β a secure UUID used for all API requests:
```
User β Sign in β Identity Provider β Session Token β All Reeve APIs
```
Session tokens:
* **Expire after 30 days** of inactivity
* **Can be revoked** instantly from Settings
* **Are opaque** β they don't carry claims like JWTs, so revocation is immediate
Gateway Authentication [#gateway-authentication]
If you self-host or use the CLI, the local gateway uses a **gateway token** for API access:
```json
{
"gateway": {
"auth": {
"token": "your-secret-token"
}
}
}
```
The onboarding wizard generates this automatically. All requests to the gateway must include this token.
For details on gateway auth, see [Gateway Authentication](/docs/gateway/authentication).
LLM Provider Keys [#llm-provider-keys]
User authentication and LLM provider authentication are **separate systems**:
* **User auth** β Signs you into Reeve (handled by this page)
* **LLM keys** β Connects Reeve to AI model providers like Anthropic or OpenAI
On the **Free** and **Pro** plans, you bring your own API keys (BYOK). On **Team** and **Cloud** plans, managed keys are available.
See [API Keys](/docs/auth/api-keys) for setting up LLM provider authentication.
Security [#security]
* All authentication happens over **HTTPS**
* Passwords are hashed with industry-standard algorithms
* Rate limiting and brute-force protection are built in
* OAuth state tokens expire after 10 minutes (CSRF protection)
* Session tokens are revocable server-side without waiting for expiry
* CORS is configured for authorized domains only
For a deeper technical dive into Reeve's authentication architecture β including service-to-service auth, guest mode, and cloud vs. local auth differences β see the [Authentication Overview](/docs/auth/overview).
Technical deep dive into the auth architecture
Set up LLM provider keys (Anthropic, OpenAI, etc.)
# Authentication Overview (/docs/auth/overview)
Authentication Overview [#authentication-overview]
Reeve uses a layered authentication system. Users authenticate via Clerk, APIs use session tokens, and services communicate with shared secrets.
User Authentication [#user-authentication]
How Sign-in Works [#how-sign-in-works]
Users authenticate through [Clerk](https://clerk.com) with three options:
| Method | How it works |
| -------------------- | ----------------------------------------------------------- |
| **Magic link** | Enter your email β click the link in your inbox β signed in |
| **Google OAuth** | One-click sign-in with your Google account |
| **Email + password** | Traditional credentials |
After authentication, Clerk issues a JWT. The frontend uses this JWT to request a **session token** from the Services API.
Session Tokens [#session-tokens]
For ongoing API access, Reeve issues session tokens β UUIDs stored in the database:
```
User β Clerk (magic link) β JWT β Services API β Session Token β All APIs
```
Session tokens are what the frontend, desktop app, and gateway use for all subsequent requests. They expire after 30 days of inactivity and can be revoked instantly.
Unlike JWTs, session tokens are opaque UUIDs β they don't carry claims and can be invalidated server-side without waiting for expiry.
Desktop App Authentication [#desktop-app-authentication]
The desktop app uses a device auth flow:
1. Desktop app opens a browser to the Clerk sign-in page
2. User authenticates via Clerk
3. Browser redirects back to the desktop app with an auth code
4. Desktop app exchanges the code for a session token
5. Token is stored locally for all API calls
The session token is refreshed automatically. You only need to sign in once.
Gateway Authentication [#gateway-authentication]
The local gateway uses a simpler model β a **gateway token** that secures the API:
```json
{
"gateway": {
"auth": {
"token": "your-secret-token"
}
}
}
```
When set, all requests to the gateway must include this token. The onboarding wizard generates one automatically.
Service-to-Service Auth [#service-to-service-auth]
The gateway communicates with the Reeve Services API (cloud backend) using a shared secret:
```
Gateway β Services API
Header: X-Reeve-Services-Token:
```
This token is set via the `REEVE_SERVICES_TOKEN` environment variable on both sides. It's never exposed to users or agents.
Auth Context [#auth-context]
Every authenticated request provides user context:
| Field | Description |
| ---------- | ------------------------------------ |
| `user_id` | Clerk user ID |
| `email` | User email address |
| `is_guest` | Whether this is a guest/demo session |
Guest Mode [#guest-mode]
For unauthenticated demo access, a guest mode provides read-only access to demo data:
```
GET /api/dashboard
X-Guest-ID: guest_demo_123
```
No write operations are permitted in guest mode.
Cloud vs Local Auth [#cloud-vs-local-auth]
| Feature | Cloud | Local (Desktop/CLI) |
| ------------- | --------------------------------- | ------------------------------------ |
| User sign-in | Clerk (magic link, Google, email) | Optional β connects to cloud account |
| API auth | Session tokens | Gateway token |
| LLM keys | Managed or BYOK | Always BYOK (your own keys) |
| Service comms | Service token (automatic) | Service token (if cloud-connected) |
Security [#security]
* All tokens are transmitted over HTTPS only
* Session tokens are UUIDs β revocable instantly server-side
* Clerk handles password hashing, rate limiting, and brute-force protection
* Service-to-service tokens never leave server environments
* CORS is configured for the frontend domain only
* OAuth state tokens have a 10-minute TTL for CSRF protection
For LLM provider API keys (Anthropic, OpenAI, etc.), see [API Keys](/docs/auth/api-keys). User authentication and LLM authentication are separate systems.
# API Reference (/docs/agents/api-reference)
Agents API Reference [#agents-api-reference]
Agent management is exposed through JSON-RPC bridge methods (for the frontend and programmatic access) and CLI commands (for humans).
Bridge Methods (JSON-RPC) [#bridge-methods-json-rpc]
These methods are registered in `gateway/server-bridge-methods-agents.js` and wrap the existing CLI logic.
agents.list [#agentslist]
List all agents with status and health info.
```typescript
// Request
{ method: "agents.list" }
// Response
{
"agents": [
{
"id": "marketing-manager",
"name": "Marketing Manager",
"role": "manager",
"model": "claude-opus-4-6",
"workspace": "/path/to/workspace",
"status": "active",
"health": { "ok": true, "issues": [] },
"identity": { "name": "Marketing Manager", "emoji": "π" },
"bindings": ["slack", "discord"]
}
]
}
```
agents.get [#agentsget]
Get detailed agent info including health check results.
```typescript
// Request
{ method: "agents.get", params: { agentId: "marketing-manager" } }
// Response
{
"agent": { /* full agent config */ },
"health": {
"ok": true,
"checks": [
{ "name": "workspace", "ok": true },
{ "name": "agents_md", "ok": true },
{ "name": "soul_md", "ok": true },
{ "name": "config", "ok": true }
]
}
}
```
agents.create [#agentscreate]
Create a new agent with role and optional config overrides.
```typescript
// Request
{
method: "agents.create",
params: {
agentId: "ad-optimizer",
role: "worker",
workspace: "/path/to/workspace",
config: {
model: "claude-sonnet-4-6",
team: "marketing"
}
}
}
// Response
{ "agent": { /* created agent entry */ }, "scaffolded": true }
```
agents.update [#agentsupdate]
Update an existing agent's config.
```typescript
// Request
{
method: "agents.update",
params: {
agentId: "marketing-manager",
config: {
model: "claude-opus-4-6",
heartbeat: { "every": "1h" }
}
}
}
// Response
{ "agent": { /* updated agent entry */ } }
```
agents.delete [#agentsdelete]
Remove an agent. Optionally clean up workspace.
```typescript
// Request
{
method: "agents.delete",
params: {
agentId: "old-worker",
cleanWorkspace: true // Also trash workspace directory
}
}
// Response
{ "deleted": true, "agentId": "old-worker" }
```
agents.doctor [#agentsdoctor]
Run health checks on a specific agent or all agents.
```typescript
// Request
{ method: "agents.doctor", params: { agentId: "marketing-manager" } }
// Response
{
"ok": true,
"checks": [
{ "name": "workspace_exists", "ok": true },
{ "name": "agents_md", "ok": true },
{ "name": "soul_md", "ok": true },
{ "name": "config_valid", "ok": true },
{ "name": "model_available", "ok": true }
]
}
```
agents.roles [#agentsroles]
Get available roles and their defaults.
```typescript
// Request
{ method: "agents.roles" }
// Response
{
"roles": {
"coordinator": { "model": "claude-opus-4-6", "heartbeat": { "every": "30m" }, ... },
"manager": { "model": "claude-opus-4-6", "heartbeat": { "every": "2h" }, ... },
"worker": { "model": "claude-sonnet-4-6", "heartbeat": null, ... },
"research": { "model": "claude-opus-4-6", "heartbeat": { "every": "4h" }, ... },
"assistant": { "model": "claude-opus-4-6", "heartbeat": { "every": "2h" }, ... }
}
}
```
CLI Commands [#cli-commands]
reeve agents list [#reeve-agents-list]
List all agents with identity, workspace, model, and channel bindings.
```bash
reeve agents list
```
Output:
```
marketing-manager π Marketing Manager
Role: manager | Model: claude-opus-4-6
Workspace: /Users/matt/reeve-workspaces/marketing
Channels: slack, discord
Heartbeat: every 2h
frontend-worker β‘ Frontend Dev
Role: worker | Model: claude-sonnet-4-6
Workspace: /Users/matt/reeve-workspaces/frontend
Channels: (none)
Heartbeat: disabled
```
reeve agents add [#reeve-agents-add]
Interactive wizard to create a new agent.
```bash
reeve agents add
reeve agents add --name my-agent --role worker --workspace ~/ws/my-agent
```
reeve agents inspect [#reeve-agents-inspect-agent-id]
Show detailed config and health status.
```bash
reeve agents inspect marketing-manager
```
reeve agents doctor [--fix] [#reeve-agents-doctor---fix]
Health check for all agents. With `--fix`, auto-repairs missing files and config issues.
```bash
reeve agents doctor
reeve agents doctor --fix
```
reeve agents delete [#reeve-agents-delete-agent-id]
Remove an agent from config. Optionally trashes workspace and session data.
```bash
reeve agents delete old-worker
```
reeve agents set-identity [#reeve-agents-set-identity-agent-id]
Set display name, emoji, and theme.
```bash
reeve agents set-identity marketing-manager --name "MarketBot" --emoji "π" --theme purple
```
# Agent Configuration (/docs/agents/configuration)
Agent Configuration [#agent-configuration]
Every agent has a configuration entry in `reeve.json` that controls its model, heartbeat, tools, sandbox, subagent permissions, and more. Configuration inherits from role defaults and can be overridden per-agent.
Config Structure [#config-structure]
An agent entry in `reeve.json`:
```json
{
"agents": {
"defaults": {
"model": "claude-sonnet-4-6",
"heartbeat": { "every": "2h" },
"sandbox": { "enabled": true }
},
"list": [
{
"id": "marketing-manager",
"name": "Marketing Manager",
"role": "manager",
"workspace": "/path/to/workspace",
"agentDir": "/path/to/agent-dir",
"model": "claude-opus-4-6",
"heartbeat": { "every": "30m" },
"identity": {
"name": "Marketing Manager",
"emoji": "π",
"theme": "purple"
},
"subagents": {
"allowAgents": ["pipeline-manager", "marketing-manager"]
},
"tools": {
"disabled": ["exec"]
},
"sessionReset": {
"idleMinutes": 2880
}
}
]
}
}
```
Config Resolution Order [#config-resolution-order]
Agent configuration is resolved in this order (later wins):
1. **Role defaults** β From `config/role-defaults.js` based on the agent's role
2. **Agent defaults** β From `agents.defaults` in `reeve.json`
3. **Agent entry** β From the specific agent in `agents.list`
4. **Config layers** β Universal β org β individual β workspace (see [Config Layers](/docs/config/layers))
Key Configuration Fields [#key-configuration-fields]
Model [#model]
Which LLM powers the agent:
```json
{
"model": "claude-opus-4-6"
}
```
Available models depend on your configured [providers](/docs/providers).
Heartbeat [#heartbeat]
How often the agent checks in autonomously:
```json
{
"heartbeat": {
"every": "2h"
}
}
```
Set to `null` to disable heartbeats (common for workers that only respond when invoked).
Subagent Policy [#subagent-policy]
Which agents this agent can spawn:
```json
{
"subagents": {
"allowAgents": ["pipeline-manager", "worker-1", "worker-2"]
}
}
```
Special values:
* `["*"]` β Can spawn any agent (coordinators)
* `[""]` β Can only spawn itself (workers, assistants)
* `["pipeline-manager", ""]` β Can spawn pipeline-manager and itself (managers)
Session Reset [#session-reset]
When to reset the agent's conversation context:
```json
{
"sessionReset": {
"idleMinutes": 60
}
}
```
| Role | Default | Meaning |
| ----------- | -------------- | -------------------------- |
| Coordinator | `null` (never) | Always-on context |
| Manager | 2880 (48h) | Reset after 2 days idle |
| Worker | 60 (1h) | Quick reset for task focus |
| Research | 1440 (24h) | Reset after 1 day idle |
| Assistant | 2880 (48h) | Persistent conversation |
Tool Policies [#tool-policies]
Disable or restrict specific tools:
```json
{
"tools": {
"disabled": ["exec", "browser"],
"readonly": true
}
}
```
Sandbox [#sandbox]
Restrict file system and command execution:
```json
{
"sandbox": {
"enabled": true,
"allowedPaths": ["/path/to/workspace"],
"blockedCommands": ["rm -rf", "sudo"]
}
}
```
Modifying Agent Config [#modifying-agent-config]
Via CLI [#via-cli]
```bash
# Set identity
reeve agents set-identity marketing-manager --name "MarketBot" --emoji "π"
# Inspect current config
reeve agents inspect marketing-manager
```
Via JSON-RPC [#via-json-rpc]
```typescript
// Update agent config
agents.update({
agentId: "marketing-manager",
config: {
model: "claude-opus-4-6",
heartbeat: { every: "1h" }
}
})
```
Direct reeve.json editing [#direct-reevejson-editing]
For complex config changes, edit `reeve.json` directly. Changes take effect on next gateway restart or config reload.
The config system uses [4-layer inheritance](/docs/config/layers). Agent config in `reeve.json` is the workspace layer β it has the highest priority and overrides all other layers.
# Creating Agents (/docs/agents/creating-agents)
Creating Agents [#creating-agents]
New agents are created through the CLI wizard, non-interactive flags, or the JSON-RPC bridge API. Each method uses the same scaffolding engine under the hood.
CLI Wizard [#cli-wizard]
```bash
reeve agents add
```
The interactive wizard walks you through:
1. **Name** β Agent identifier (e.g., `marketing-manager`)
2. **Role** β Select from coordinator, manager, worker, research, assistant
3. **Workspace** β Directory for the agent's context files
4. **Team** β Optional team grouping
5. **Channel bindings** β Which messaging platforms to connect
6. **Auth** β API keys or shared auth
What gets created [#what-gets-created]
When you create an agent, the scaffolding engine:
1. Creates the workspace directory
2. Writes role-appropriate template files:
* **AGENTS.md** β Agent instructions (role-specific)
* **SOUL.md** β Personality and communication style
* **HEARTBEAT.md** β Periodic check instructions (managers/research only)
* **TOOLS.md** β Available tool documentation
* **IDENTITY.md** β Name, emoji, theme
3. Creates a `memory/` directory for persistent memory
4. Adds the agent entry to `reeve.json` with role defaults
5. Applies model, heartbeat, and subagent config from role defaults
Non-interactive mode [#non-interactive-mode]
For scripting and automation:
```bash
reeve agents add \
--name ad-optimizer \
--role worker \
--workspace ~/reeve-workspaces/ad-optimizer \
--team marketing
```
Role-Based Templates [#role-based-templates]
Each role has tailored workspace files:
Manager templates [#manager-templates]
```markdown
# AGENTS.md (manager role)
## Three-Tier Architecture
- Manager: Strategic planning, delegation, oversight
- Pipeline Manager: Complex multi-file orchestration
- Workers: Focused task execution
## Management Framework
- Spawn pipeline-manager for complex work
- Dispatch workers for focused tasks
- Monitor sub-agent health and progress
```
Worker templates [#worker-templates]
```markdown
# AGENTS.md (worker role)
## Scope
- Execute assigned tasks within your workspace
- Report completion to spawning agent
- No management framework (lean and focused)
```
Research templates [#research-templates]
```markdown
# AGENTS.md (research role)
## Research Tools
- web_search, web_fetch, browser for deep research
- Knowledge base for document ingestion
- Memory search for accumulated knowledge
```
Template variables are substituted at scaffold time: `{{agentId}}`, `{{agentName}}`, `{{role}}`, `{{description}}`, `{{workspace}}`, `{{team}}`, `{{date}}`.
Idempotent Scaffolding [#idempotent-scaffolding]
The scaffolding engine is **idempotent** β it won't overwrite existing files. This means:
* Running `reeve agents add` for an existing agent updates config but doesn't clobber workspace files
* You can safely re-scaffold after editing your AGENTS.md
* The `wx` file flag ensures no accidental overwrites
From the Frontend [#from-the-frontend]
The Cockpit's Agent Management UI (`/cockpit/settings/agents`) provides a visual interface:
1. Click **"New Agent"**
2. Fill in name, role, workspace path
3. Configure model and heartbeat overrides
4. Click **Create**
This calls `agents.create` via the JSON-RPC bridge (see [API Reference](/docs/agents/api-reference)).
The `reeve agents doctor` command checks all agents for missing files, broken paths, and config issues. Run with `--fix` to auto-repair.
# Agent Management (/docs/agents)
Agent Management [#agent-management]
Reeve's agent management system lets you create, configure, and orchestrate multiple AI agents β each with its own role, workspace, model, and behavior. Agents are first-class entities with full lifecycle management.
What's an Agent? [#whats-an-agent]
An agent is a persistent AI identity with:
* **Role** β Defines behavior defaults (coordinator, manager, worker, research, assistant)
* **Workspace** β A directory with AGENTS.md, SOUL.md, TOOLS.md, MEMORY, and other context files
* **Model** β Which LLM powers the agent (Claude Opus, Sonnet, GPT-4, etc.)
* **Channel bindings** β Which messaging platforms the agent responds on
* **Heartbeat** β Periodic check-in schedule for autonomous work
* **Subagent policy** β Which other agents it can spawn
Agent Roles [#agent-roles]
Roles provide sensible defaults for different agent types:
| Role | Model | Heartbeat | Subagents | Session Reset | Use Case |
| ------------- | ------ | --------- | --------------- | ------------- | ------------------------------- |
| `coordinator` | Opus | 30m | All (`*`) | Never | Top-level orchestrator |
| `manager` | Opus | 2h | Pipeline + self | 48h idle | Domain manager (marketing, eng) |
| `worker` | Sonnet | None | Self only | 1h idle | Task execution |
| `research` | Opus | 4h | Self only | 24h idle | Deep research, analysis |
| `assistant` | Opus | 2h | Self only | 48h idle | General-purpose helper |
Roles are defined in `config/role-defaults.js`. When you create an agent with a role, these defaults are applied β but you can override any of them.
Quick Overview [#quick-overview]
```bash
# List all agents
reeve agents list
# Add a new agent
reeve agents add
# Inspect agent config + health
reeve agents inspect marketing-manager
# Health check with auto-fix
reeve agents doctor --fix
# Remove an agent
reeve agents delete old-worker
```
Architecture [#architecture]
Agent management spans several gateway components:
| Component | File | Purpose |
| ---------------------- | ----------------------------------------- | ------------------------------------------------------------- |
| **Role defaults** | `config/role-defaults.js` | 5 roles with model/heartbeat/subagent defaults |
| **Scaffolding engine** | `agents/scaffolding.js` | Creates workspace files from role templates |
| **Config helpers** | `commands/agents.config.js` | `listAgentEntries`, `applyAgentConfig`, `buildAgentSummaries` |
| **CLI commands** | `commands/agents.commands.*.js` | add, list, inspect, doctor, delete, identity |
| **Bridge methods** | `gateway/server-bridge-methods-agents.js` | JSON-RPC API for frontend |
| **Templates** | `docs/reference/templates/roles/` | Role-specific AGENTS.md, SOUL.md, HEARTBEAT.md |
Templates, roles, and the CLI wizard
Per-agent config, model selection, tool policies
Spawning, coordination, and pipeline managers
Bridge methods and CLI commands
# Agent Memory (/docs/agents/memory)
Agent Memory [#agent-memory]
Reeve memory is **plain Markdown in the agent workspace**. The files are the source of truth β the model only "remembers" what gets written to disk.
Memory Files [#memory-files]
Every agent workspace has two memory layers:
| File | Purpose | When to use |
| ---------------------- | ------------------------ | --------------------------------------------------- |
| `MEMORY.md` | Curated long-term memory | Decisions, preferences, key facts, stable knowledge |
| `memory/YYYY-MM-DD.md` | Daily log (append-only) | Running notes, observations, session context |
```
~/reeve/
βββ MEMORY.md # Long-term memory
βββ memory/
βββ 2026-03-01.md # Daily log
βββ 2026-03-02.md
βββ 2026-03-03.md
```
At session start, the agent reads today's and yesterday's daily logs. `MEMORY.md` is loaded in the main (private) session only β never in group contexts.
Writing Memory [#writing-memory]
Agents write memory in three ways:
1. You ask them to [#1-you-ask-them-to]
Tell your agent *"remember this"* and it writes to the appropriate file. This is the most reliable way to store important information.
2. Automatic memory flush [#2-automatic-memory-flush]
When a session approaches [compaction](/docs/concepts/compaction) (context window filling up), Reeve triggers a silent memory flush. The agent writes durable notes to `memory/YYYY-MM-DD.md` before the context is summarized.
This is controlled by config:
```json
{
"agents": {
"defaults": {
"compaction": {
"memoryFlush": {
"enabled": true,
"softThresholdTokens": 4000
}
}
}
}
}
```
3. Agent initiative [#3-agent-initiative]
Well-configured agents learn to write memory proactively β noting decisions, preferences, and important context without being asked.
Reading Memory [#reading-memory]
Agents access memory through two tools:
* **`memory_search`** β Semantic search across all memory files
* **`memory_get`** β Read a specific memory file by path
Semantic Search [#semantic-search]
Memory search uses vector embeddings to find relevant notes even when wording differs:
```
Query: "What model does the marketing team prefer?"
β Finds: "2026-02-15: Marketing decided to use Claude Sonnet for all content generation"
```
This works because semantic search matches meaning, not just keywords. It also supports hybrid search (vector + BM25 keyword matching) for exact tokens like IDs and code symbols.
Search Configuration [#search-configuration]
Use OpenAI or Gemini for embedding generation:
```json
{
"agents": {
"defaults": {
"memorySearch": {
"provider": "openai",
"model": "text-embedding-3-small"
}
}
}
}
```
Reeve auto-selects a provider if you don't configure one:
1. OpenAI (if an API key is available)
2. Gemini (if a Gemini key is available)
3. Disabled (until configured)
Run embeddings locally with no API calls:
```json
{
"agents": {
"defaults": {
"memorySearch": {
"provider": "local"
}
}
}
}
```
Uses a bundled GGUF model (\~600 MB, auto-downloaded on first use). Requires `node-llama-cpp` native build:
```bash
pnpm approve-builds # Select node-llama-cpp
pnpm rebuild node-llama-cpp
```
Hybrid Search [#hybrid-search]
Combine semantic and keyword search for best results:
```json
{
"agents": {
"defaults": {
"memorySearch": {
"query": {
"hybrid": {
"enabled": true,
"vectorWeight": 0.7,
"textWeight": 0.3
}
}
}
}
}
}
```
Hybrid search excels at finding both natural language matches and exact tokens (IDs, error strings, code symbols).
Index Storage [#index-storage]
Memory embeddings are stored in a per-agent SQLite database:
```
~/.reeve/memory/.sqlite
```
The index automatically rebuilds when you change the embedding provider, model, or chunking parameters. A file watcher detects memory changes and updates the index in the background.
Session Memory (Experimental) [#session-memory-experimental]
Optionally index session transcripts for search:
```json
{
"agents": {
"defaults": {
"memorySearch": {
"experimental": { "sessionMemory": true },
"sources": ["memory", "sessions"]
}
}
}
}
```
This lets agents search past conversations, not just memory files. Session indexing is opt-in and runs asynchronously.
Best Practices [#best-practices]
* **Tell agents what to remember** β If something is important, say "remember this"
* **Review `MEMORY.md` periodically** β Curate it like you would notes; remove outdated info
* **Daily logs are disposable** β They accumulate and get searched, but old ones don't need maintenance
* **Keep memory files Markdown** β The system expects `.md` files; other formats won't be indexed
* **Enable semantic search** β It's the difference between "grep" and "understanding"
***
The Three Content Layers [#the-three-content-layers]
Every Reeve agent maintains memory across three source layers:
1. Session Transcripts [#1-session-transcripts]
Every conversation is stored in full as a raw transcript. These aren't just logs β they're indexed and searchable. Reeve can surface a decision you made three months ago, a bug that was fixed last week, or a pattern that keeps recurring across your sessions.
**How it works:** Session files are chunked into conversational exchanges (question + response = one unit), embedded with a language model, and stored in a local semantic index. Searchable within seconds of the conversation ending.
2. Daily Notes [#2-daily-notes]
After each working session, Reeve writes a structured daily note (`memory/YYYY-MM-DD.md`). This is the "what happened today" layer β summarized, organized, and immediately readable by both the agent on its next session and by you.
Daily notes are the reconciliation point for cloud sync: if two sessions write to the same day's note (e.g., you worked in both local and Slack that day), the entries are *append-merged* β nothing is lost.
3. Long-Term Memory (MEMORY.md) [#3-long-term-memory-memorymd]
The curated, durable layer. Decisions that stick, preferences, recurring context, key relationships, product direction. Reeve maintains this file and references it at the start of every session. It's what makes a year-old agent feel like a senior team member rather than a new hire.
This file is yours β you can read it, edit it, and shape it directly.
***
The Semantic Index [#the-semantic-index]
On top of the three content layers sits a unified semantic index. All three layers β transcripts, daily notes, and long-term memory β are chunked and embedded using your configured embedding model (OpenAI, Gemini, or local via Ollama).
When you ask your agent to recall something, or when the agent searches its own memory, it runs a **hybrid search**: vector similarity (semantic meaning) + full-text BM25 (keyword precision), merged and ranked. The result is accurate recall even when you can't remember the exact words you used.
***
Per-Agent Memory [#per-agent-memory]
Every agent has its own memory workspace. An agent's memory is specialized to its role β your marketing agent accumulates brand voice, competitor notes, and campaign history; your dev agent accumulates codebase context, architectural decisions, and bug patterns.
This specialization is intentional. You don't want your dev agent's memory polluted with campaign data, and vice versa. Each agent develops genuine expertise over time.
***
The Scope Hierarchy [#the-scope-hierarchy]
Memory in Reeve is scoped at three levels, merged at query time:
| Scope | What it contains | Weight |
| ------------ | ------------------------------------------------------------------ | ---------------------- |
| **Agent** | The agent's own workspace | 1.0 (primary) |
| **Org** | Shared team memory β auto-promoted insights, cross-agent learnings | 0.9 |
| **Personal** | A specific human user's workspace context | 0.8 (DM sessions only) |
In single-user local mode, only the agent scope is active. In cloud/team mode, all three scopes are searched and merged using Reciprocal Rank Fusion.
***
Local β Cloud Sync [#local--cloud-sync]
This feature is in progress as of March 2026 and not yet live in production.
**Vision:** You work locally with your agent in the morning. You pick up the same conversation in Slack from your phone 30 seconds later β full context, no re-explaining.
**How it works:** When a memory file changes locally, a file watcher triggers a sync to S3 within \~1β5 seconds. When a cloud session starts, it pulls the latest memory state from S3 before the first message. Conflict resolution:
* Daily notes (`memory/YYYY-MM-DD.md`): append-merge β both versions are preserved
* `MEMORY.md` and other files: last-writer-wins based on modification time
The sync engine is fully built. Cloud provisioning (S3 bucket, STS credentials endpoint) is in progress.
***
Shared Agent Memory [#shared-agent-memory]
This feature is in progress as of March 2026 and not yet live in production.
**Vision:** Your whole team works with the same agent. Dev 1 ships a feature; Dev 2 picks up code review with the agent already knowing the full context. A new team member joins; the agent already knows your codebase, your decisions, your preferences.
**How it works:** When an agent writes a daily summary, it automatically extracts "promotable insights" β decisions, bugs fixed, process changes, config updates β and promotes them to the shared org memory layer. These insights are attributed (`[date | user | agent]`) and accumulate over time. Any team member's session can query this shared layer.
**What gets auto-promoted:** Lines containing decision language ("decided," "chose," "going with"), bug language ("fixed," "resolved"), process changes ("migrated," "restructured"), and configuration changes.
The auto-promotion pipeline and shared memory search are built. End-to-end testing with real teams is in progress.
***
Knowledge Graph Memory [#knowledge-graph-memory]
This feature is in progress as of March 2026 and not yet live in production.
**Vision:** Reeve doesn't just store what you told it β it understands the relationships between things. "This bug was caused by that dependency change." "That decision affects these three features." "This person owns that project." A graph you can traverse, not just search.
**How it works:** As sessions are archived, a background process runs entity and relationship extraction using Claude Haiku. Entities are typed (person, project, decision, concept, tool, brand, codebase, bug, feature, config). Relationships use 12 constrained edge types: `owns`, `created`, `decided`, `uses`, `affects`, `depends_on`, `part_of`, `fixed_by`, `caused_by`, `blocked_by`, `related_to`, `replaced_by`.
Connected entity clusters are detected and summarized by community β so instead of retrieving individual facts, Reeve can retrieve a coherent picture of a topic cluster ("everything related to the auth system rewrite").
**Under the hood:** This runs on the same SQLite file as the rest of memory β entities, relationships, and community summaries are stored as tables and queried alongside the semantic index. No new infrastructure required.
Extraction pipeline, community detection, and graph search are built and merged. Wiring the graph results into the `memory_search` tool response is in progress.
***
Memory files are just Markdown β you can read and edit them directly with any text editor. Changes are picked up by the search index automatically.
See the full [Memory reference](/docs/concepts/memory) for all configuration options including caching, batch indexing, and SQLite vector acceleration.
# Multi-Agent Coordination (/docs/agents/multi-agent)
Multi-Agent Coordination [#multi-agent-coordination]
Reeve supports multi-agent architectures where agents spawn, delegate to, and coordinate with each other. The three-tier model provides a natural hierarchy for complex work.
Three-Tier Architecture [#three-tier-architecture]
```
Coordinator (top-level)
ββ Domain Managers (marketing, engineering, ops)
ββ Pipeline Manager (complex orchestration)
ββ Workers (focused task execution)
```
Coordinator [#coordinator]
The single top-level agent with `allowAgents: ["*"]`. It routes incoming requests to the appropriate domain manager and has full visibility across all agents.
Domain Managers [#domain-managers]
Specialized agents that own a domain (marketing, engineering, support). They:
* Receive tasks from the coordinator or users
* Spawn pipeline-manager for complex multi-step work
* Dispatch workers for focused tasks
* Monitor progress and report back
Pipeline Manager [#pipeline-manager]
A special orchestration agent spawned by managers for complex, multi-file, multi-phase work. It:
* Analyzes task scope and identifies independent work tracks
* Spawns parallel workers
* Monitors worker completion
* Verifies results before reporting done
Workers [#workers]
Focused execution agents. They:
* Receive a single, well-scoped task
* Execute it within their workspace
* Report completion to their spawning agent
* Reset quickly (1h idle timeout)
Spawning Sub-Agents [#spawning-sub-agents]
Agents spawn sub-agents using the `sessions_spawn` tool:
```typescript
sessions_spawn({
label: "write-landing-page",
agentId: "frontend-worker",
message: "Create a new landing page at /pages/launch.tsx with hero section, features grid, and CTA. Use the brand colors from BRAND.md.",
runTimeoutSeconds: 600
})
```
Spawn parameters [#spawn-parameters]
| Parameter | Type | Description |
| ------------------- | ------ | --------------------------------------------- |
| `agentId` | string | Which agent to spawn |
| `label` | string | Human-readable label for tracking |
| `message` | string | The task/instructions to send |
| `runTimeoutSeconds` | number | Kill after this many seconds (default varies) |
Monitoring spawned agents [#monitoring-spawned-agents]
```typescript
// List active sessions
sessions_list()
// Check specific session history
sessions_history({ sessionId: "sess_abc123" })
```
Parallel Dispatch [#parallel-dispatch]
For maximum throughput, managers dispatch multiple workers simultaneously:
```typescript
// Spawn 3 workers in parallel
sessions_spawn({
label: "backend-api",
agentId: "backend-worker",
message: "Add GET /api/products endpoint..."
})
sessions_spawn({
label: "frontend-ui",
agentId: "frontend-worker",
message: "Create ProductList component at..."
})
sessions_spawn({
label: "write-tests",
agentId: "test-worker",
message: "Write integration tests for..."
})
```
Each worker runs independently. The manager polls `sessions_list` to monitor completion.
Pipeline Manager Pattern [#pipeline-manager-pattern]
For complex work that spans multiple files or repos, managers spawn the pipeline-manager:
```typescript
sessions_spawn({
label: "implement-billing",
agentId: "pipeline-manager",
message: `
Implement the billing integration:
1. Backend: Add Stripe webhooks to /api/billing
2. Frontend: Create billing settings page
3. Services: Add credit deduction endpoints
4. Tests: Integration tests for the full flow
Use parallel workers for independent tracks.
`,
runTimeoutSeconds: 900
})
```
The pipeline manager then:
1. Reads the task and creates a plan
2. Identifies independent work tracks
3. Spawns workers for each track with focused instructions
4. Monitors completion and verifies results
5. Reports back to the spawning manager
Subagent Policies [#subagent-policies]
The `allowAgents` config controls which agents can be spawned:
```json
// Coordinator β can spawn anything
{ "subagents": { "allowAgents": ["*"] } }
// Manager β can spawn pipeline-manager and self
{ "subagents": { "allowAgents": ["pipeline-manager", "marketing-manager"] } }
// Worker β can only spawn self (for retries)
{ "subagents": { "allowAgents": ["frontend-worker"] } }
```
If an agent tries to spawn an agent not in its `allowAgents` list, the spawn is blocked. This prevents workers from escalating privileges by spawning coordinators or managers.
Communication Patterns [#communication-patterns]
Direct messaging [#direct-messaging]
Agents can send messages to other agents' sessions:
```typescript
sessions_send({
target: "marketing-manager",
message: "Task complete: landing page deployed to /pages/launch"
})
```
Announcements [#announcements]
Broadcast to multiple agents:
```typescript
sessions_announce({
targets: ["marketing-manager", "eng-manager", "ops-manager"],
message: "Billing integration is live. Please update your respective docs."
})
```
Goal-based coordination [#goal-based-coordination]
For sustained work, use [Goals](/docs/goals) instead of one-off spawns. Goals persist across sessions and provide budget controls, checkpoints, and progress tracking.
# Agent Overview (/docs/agents/overview)
Agent Overview [#agent-overview]
An agent is a persistent AI identity with its own role, model, workspace, and memory. Agents are the core building block of Reeve β they do the work.
What Makes an Agent [#what-makes-an-agent]
Every agent has:
| Property | What it controls |
| -------------------- | ------------------------------------------------------------ |
| **Role** | Behavior defaults (model, heartbeat, permissions) |
| **Model** | Which LLM powers it (Claude Opus, Sonnet, GPT-4, etc.) |
| **Workspace** | Directory with AGENTS.md, SOUL.md, MEMORY.md, and daily logs |
| **Channel bindings** | Which messaging platforms it responds on |
| **Heartbeat** | Periodic check-in schedule for autonomous work |
| **Subagent policy** | Which other agents it can spawn |
| **Tool policy** | Which tools it can use |
Roles [#roles]
Roles provide sensible defaults so you don't have to configure everything from scratch:
| Role | Model | Heartbeat | Use case |
| ------------- | ------ | --------- | ------------------------------------------------------ |
| `coordinator` | Opus | 30 min | Top-level orchestrator β routes work, never resets |
| `manager` | Opus | 2 hours | Domain lead (marketing, engineering) β spawns workers |
| `worker` | Sonnet | None | Task executor β fast, cheap, resets after 1h idle |
| `research` | Opus | 4 hours | Deep analysis β resets after 24h idle |
| `assistant` | Opus | 2 hours | General helper β conversational, resets after 48h idle |
Roles are starting points β you can override any default per agent.
Single vs Multi-Agent [#single-vs-multi-agent]
Single Agent [#single-agent]
The simplest setup: one agent handles everything. Good for personal use, small teams, or getting started.
```
You ββ Reeve (assistant)
```
Multi-Agent [#multi-agent]
For complex work, agents specialize and coordinate:
```
Coordinator
βββ Marketing Manager
β βββ Content Writer (worker)
β βββ Ad Analyst (worker)
βββ Engineering Manager
βββ Frontend Dev (worker)
βββ Backend Dev (worker)
```
Managers spawn workers for parallel execution, coordinate results, and report back. Workers are cheap (Sonnet) and reset quickly. Managers and coordinators maintain longer context.
See [Multi-Agent](/docs/agents/multi-agent) for the full coordination system.
Built-in vs Custom Agents [#built-in-vs-custom-agents]
Built-in [#built-in]
Every Reeve installation starts with a default agent (typically `assistant` role). This agent is ready to go with sensible defaults β you just need an LLM API key.
Custom [#custom]
Create agents tailored to specific roles:
```bash
reeve agents add
```
The interactive wizard asks for:
* **Name** β A human-readable name (e.g., "Marketing Manager")
* **Role** β One of the five built-in roles
* **Model** β Which LLM to use
* **Workspace** β Where to store config and memory
Or create agents from the Cockpit at **Settings β Agents β New Agent**.
Agent Lifecycle [#agent-lifecycle]
```
Create β Configure β Active β [Sessions, Memory, Goals] β Archive/Delete
```
1. **Create** β Via CLI wizard or Cockpit UI
2. **Configure** β Set model, tools, heartbeat, workspace files
3. **Active** β Agent responds to messages, runs heartbeats, executes goals
4. **Memory grows** β Daily logs accumulate, MEMORY.md gets curated
5. **Archive/Delete** β When no longer needed
Agents persist across gateway restarts. Their memory, configuration, and workspace survive indefinitely.
AGENTS.md, SOUL.md, USER.md, and per-agent config
How agents remember things long-term
Available tools and custom tool creation
Step-by-step agent creation
Agents are identified by a unique ID (e.g., `marketing-manager`). This ID is used in configuration, CLI commands, and inter-agent communication.
# Agent Tools (/docs/agents/tools)
Agent Tools [#agent-tools]
Tools are capabilities that agents use during conversations β running commands, searching the web, controlling a browser, sending messages, and more. Reeve provides a rich set of built-in tools, plus connector tools from your linked platforms.
Built-in Tools [#built-in-tools]
File System [#file-system]
| Tool | What it does |
| ------------- | ------------------------------- |
| `read` | Read file contents |
| `write` | Create or overwrite files |
| `edit` | Find-and-replace edits in files |
| `apply_patch` | Multi-hunk structured patches |
Execution [#execution]
| Tool | What it does |
| --------- | --------------------------------------------- |
| `exec` | Run shell commands with background support |
| `process` | Manage background processes (poll, log, kill) |
Web [#web]
| Tool | What it does |
| ------------ | ----------------------------------- |
| `web_search` | Search the web via Brave Search |
| `web_fetch` | Fetch and extract content from URLs |
Browser [#browser]
| Tool | What it does |
| --------- | --------------------------------------------------------------------- |
| `browser` | Full browser automation β navigate, snapshot, click, type, screenshot |
Communication [#communication]
| Tool | What it does |
| --------- | ----------------------------------------------------------------- |
| `message` | Send messages across WhatsApp, Telegram, Discord, Slack, and more |
Sessions & Agents [#sessions--agents]
| Tool | What it does |
| ------------------ | ---------------------------------- |
| `sessions_list` | List active sessions |
| `sessions_history` | View session transcripts |
| `sessions_send` | Send a message to another session |
| `sessions_spawn` | Create a sub-agent session |
| `session_status` | Check current session state |
| `agents_list` | List agents available for spawning |
Memory [#memory]
| Tool | What it does |
| --------------- | --------------------------------- |
| `memory_search` | Semantic search over memory files |
| `memory_get` | Read a specific memory file |
Automation [#automation]
| Tool | What it does |
| --------- | ----------------------------------------- |
| `cron` | Manage scheduled jobs and wakeups |
| `gateway` | Restart, configure, or update the gateway |
Devices [#devices]
| Tool | What it does |
| -------- | ------------------------------------------------------ |
| `nodes` | Control paired devices β camera, screen, notifications |
| `canvas` | Render UI on paired device screens |
| `image` | Analyze images with a vision model |
Connector Tools [#connector-tools]
When you connect external platforms via [Connectors](/docs/connectors/overview), agents get access to platform-specific tools:
| Tool | Source | What it does |
| ----------------- | ------------------ | ------------------------------------ |
| `reeve_shopify` | Shopify | Products, orders, customers, revenue |
| `reeve_analytics` | PostHog / GA4 | Traffic, funnels, top pages |
| `reeve_ads` | Meta / Google | Campaign performance, ad generation |
| `reeve_email` | Klaviyo | Email campaigns, flows, segments |
| `reeve_revenue` | Stripe | MRR, LTV, subscriptions |
| `reeve_social` | Connected accounts | Social metrics, trends |
| `reeve_support` | Gorgias / Zendesk | Support tickets |
| `reeve_brand` | Brand profiler | Brand analysis, competitive intel |
Tool Profiles [#tool-profiles]
Profiles restrict which tools an agent can access:
| Profile | Tools included |
| ----------- | --------------------------------------------- |
| `full` | All tools (default) |
| `coding` | File system, runtime, sessions, memory, image |
| `messaging` | Message, sessions list/history/send/status |
| `minimal` | Session status only |
Set a profile globally or per-agent:
```json
{
"tools": {
"profile": "coding"
}
}
```
Per-agent override:
```json
{
"agents": {
"list": [{
"id": "support-bot",
"tools": { "profile": "messaging" }
}]
}
}
```
Allow / Deny Lists [#allow--deny-lists]
Fine-tune beyond profiles with allow and deny lists:
```json
{
"tools": {
"profile": "coding",
"allow": ["web_search", "web_fetch"],
"deny": ["exec"]
}
}
```
Deny always wins over allow. Use `group:*` shorthands for convenience:
| Group | Tools |
| ------------------ | ----------------------------------------------------------------------------------- |
| `group:runtime` | exec, bash, process |
| `group:fs` | read, write, edit, apply\_patch |
| `group:sessions` | sessions\_list, sessions\_history, sessions\_send, sessions\_spawn, session\_status |
| `group:memory` | memory\_search, memory\_get |
| `group:web` | web\_search, web\_fetch |
| `group:ui` | browser, canvas |
| `group:automation` | cron, gateway |
| `group:messaging` | message |
Custom Tools via Plugins [#custom-tools-via-plugins]
Extend the tool set with [plugins](/docs/plugins/overview):
1. Create a plugin manifest defining your tool's schema
2. Implement the tool handler
3. Register the plugin in your config
Plugins can add both tools and CLI commands. See [Agent Tools Plugin](/docs/plugins/agent-tools) for the full guide.
Skills [#skills]
[Skills](/docs/tools/skills) are prompt-level guidance that teach agents *how* to use tools effectively. While tools define *what* an agent can do, skills define *how* it should approach a task.
Built-in skills include:
* `frontend-design` β UI development best practices
* `test-driven-development` β Test-first coding workflows
* `systematic-debugging` β Structured debugging methodology
* `using-git-worktrees` β Git branching strategies
Configure skills per-agent or use them on-demand via slash commands.
Tools are presented to the agent in two ways: as structured function schemas (for the model API) and as human-readable text in the system prompt. If a tool doesn't appear in either, the agent cannot use it.
# Auth monitoring (/docs/automation/auth-monitoring)
Auth monitoring [#auth-monitoring]
Reeve exposes OAuth expiry health via `reeve models status`. Use that for
automation and alerting; scripts are optional extras for phone workflows.
Preferred: CLI check (portable) [#preferred-cli-check-portable]
```bash
reeve models status --check
```
Exit codes:
* `0`: OK
* `1`: expired or missing credentials
* `2`: expiring soon (within 24h)
This works in cron/systemd and requires no extra scripts.
Optional scripts (ops / phone workflows) [#optional-scripts-ops--phone-workflows]
These live under `scripts/` and are **optional**. They assume SSH access to the
gateway host and are tuned for systemd + Termux.
* `scripts/claude-auth-status.sh` now uses `reeve models status --json` as the
source of truth (falling back to direct file reads if the CLI is unavailable),
so keep `reeve` on `PATH` for timers.
* `scripts/auth-monitor.sh`: cron/systemd timer target; sends alerts (ntfy or phone).
* `scripts/systemd/reeve-auth-monitor.{service,timer}`: systemd user timer.
* `scripts/claude-auth-status.sh`: Claude Code + Reeve auth checker (full/json/simple).
* `scripts/mobile-reauth.sh`: guided reβauth flow over SSH.
* `scripts/termux-quick-auth.sh`: oneβtap widget status + open auth URL.
* `scripts/termux-auth-widget.sh`: full guided widget flow.
* `scripts/termux-sync-widget.sh`: sync Claude Code creds β Reeve.
If you donβt need phone automation or systemd timers, skip these scripts.
# Cron jobs (Gateway scheduler) (/docs/automation/cron-jobs)
Cron jobs (Gateway scheduler) [#cron-jobs-gateway-scheduler]
> **Cron vs Heartbeat?** See [Cron vs Heartbeat](/docs/automation/cron-vs-heartbeat) for guidance on when to use each.
Cron is the Gatewayβs built-in scheduler. It persists jobs, wakes the agent at
the right time, and can optionally deliver output back to a chat.
If you want *βrun this every morningβ* or *βpoke the agent in 20 minutesβ*,
cron is the mechanism.
TL;DR [#tldr]
* Cron runs **inside the Gateway** (not inside the model).
* Jobs persist under `~/.reeve/cron/` so restarts donβt lose schedules.
* Two execution styles:
* **Main session**: enqueue a system event, then run on the next heartbeat.
* **Isolated**: run a dedicated agent turn in `cron:`, optionally deliver output.
* Wakeups are first-class: a job can request βwake nowβ vs βnext heartbeatβ.
Beginner-friendly overview [#beginner-friendly-overview]
Think of a cron job as: **when** to run + **what** to do.
1. **Choose a schedule**
* One-shot reminder β `schedule.kind = "at"` (CLI: `--at`)
* Repeating job β `schedule.kind = "every"` or `schedule.kind = "cron"`
* If your ISO timestamp omits a timezone, it is treated as **UTC**.
2. **Choose where it runs**
* `sessionTarget: "main"` β run during the next heartbeat with main context.
* `sessionTarget: "isolated"` β run a dedicated agent turn in `cron:`.
3. **Choose the payload**
* Main session β `payload.kind = "systemEvent"`
* Isolated session β `payload.kind = "agentTurn"`
Optional: `deleteAfterRun: true` removes successful one-shot jobs from the store.
Concepts [#concepts]
Jobs [#jobs]
A cron job is a stored record with:
* a **schedule** (when it should run),
* a **payload** (what it should do),
* optional **delivery** (where output should be sent).
* optional **agent binding** (`agentId`): run the job under a specific agent; if
missing or unknown, the gateway falls back to the default agent.
Jobs are identified by a stable `jobId` (used by CLI/Gateway APIs).
In agent tool calls, `jobId` is canonical; legacy `id` is accepted for compatibility.
Jobs can optionally auto-delete after a successful one-shot run via `deleteAfterRun: true`.
Schedules [#schedules]
Cron supports three schedule kinds:
* `at`: one-shot timestamp (ms since epoch). Gateway accepts ISO 8601 and coerces to UTC.
* `every`: fixed interval (ms).
* `cron`: 5-field cron expression with optional IANA timezone.
Cron expressions use `croner`. If a timezone is omitted, the Gateway hostβs
local timezone is used.
Main vs isolated execution [#main-vs-isolated-execution]
Main session jobs (system events) [#main-session-jobs-system-events]
Main jobs enqueue a system event and optionally wake the heartbeat runner.
They must use `payload.kind = "systemEvent"`.
* `wakeMode: "next-heartbeat"` (default): event waits for the next scheduled heartbeat.
* `wakeMode: "now"`: event triggers an immediate heartbeat run.
This is the best fit when you want the normal heartbeat prompt + main-session context.
See [Heartbeat](/docs/gateway/heartbeat).
Isolated jobs (dedicated cron sessions) [#isolated-jobs-dedicated-cron-sessions]
Isolated jobs run a dedicated agent turn in session `cron:`.
Key behaviors:
* Prompt is prefixed with `[cron:]` for traceability.
* Each run starts a **fresh session id** (no prior conversation carry-over).
* A summary is posted to the main session (prefix `Cron`, configurable).
* `wakeMode: "now"` triggers an immediate heartbeat after posting the summary.
* If `payload.deliver: true`, output is delivered to a channel; otherwise it stays internal.
Use isolated jobs for noisy, frequent, or "background chores" that shouldn't spam
your main chat history.
Payload shapes (what runs) [#payload-shapes-what-runs]
Two payload kinds are supported:
* `systemEvent`: main-session only, routed through the heartbeat prompt.
* `agentTurn`: isolated-session only, runs a dedicated agent turn.
Common `agentTurn` fields:
* `message`: required text prompt.
* `model` / `thinking`: optional overrides (see below).
* `timeoutSeconds`: optional timeout override.
* `deliver`: `true` to send output to a channel target.
* `channel`: `last` or a specific channel.
* `to`: channel-specific target (phone/chat/channel id).
* `bestEffortDeliver`: avoid failing the job if delivery fails.
Isolation options (only for `session=isolated`):
* `postToMainPrefix` (CLI: `--post-prefix`): prefix for the system event in main.
* `postToMainMode`: `summary` (default) or `full`.
* `postToMainMaxChars`: max chars when `postToMainMode=full` (default 8000).
Model and thinking overrides [#model-and-thinking-overrides]
Isolated jobs (`agentTurn`) can override the model and thinking level:
* `model`: Provider/model string (e.g., `anthropic/claude-sonnet-4-20250514`) or alias (e.g., `opus`)
* `thinking`: Thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`; GPT-5.2 + Codex models only)
Note: You can set `model` on main-session jobs too, but it changes the shared main
session model. We recommend model overrides only for isolated jobs to avoid
unexpected context shifts.
Resolution priority:
1. Job payload override (highest)
2. Hook-specific defaults (e.g., `hooks.gmail.model`)
3. Agent config default
Delivery (channel + target) [#delivery-channel--target]
Isolated jobs can deliver output to a channel. The job payload can specify:
* `channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`
* `to`: channel-specific recipient target
If `channel` or `to` is omitted, cron can fall back to the main sessionβs βlast routeβ
(the last place the agent replied).
Delivery notes:
* If `to` is set, cron auto-delivers the agentβs final output even if `deliver` is omitted.
* Use `deliver: true` when you want last-route delivery without an explicit `to`.
* Use `deliver: false` to keep output internal even if a `to` is present.
Target format reminders:
* Slack/Discord/Mattermost (plugin) targets should use explicit prefixes (e.g. `channel:`, `user:`) to avoid ambiguity.
* Telegram topics should use the `:topic:` form (see below).
Telegram delivery targets (topics / forum threads) [#telegram-delivery-targets-topics--forum-threads]
Telegram supports forum topics via `message_thread_id`. For cron delivery, you can encode
the topic/thread into the `to` field:
* `-1001234567890` (chat id only)
* `-1001234567890:topic:123` (preferred: explicit topic marker)
* `-1001234567890:123` (shorthand: numeric suffix)
Prefixed targets like `telegram:...` / `telegram:group:...` are also accepted:
* `telegram:group:-1001234567890:topic:123`
Storage & history [#storage--history]
* Job store: `~/.reeve/cron/jobs.json` (Gateway-managed JSON).
* Run history: `~/.reeve/cron/runs/.jsonl` (JSONL, auto-pruned).
* Override store path: `cron.store` in config.
Configuration [#configuration]
```json5
{
cron: {
enabled: true, // default true
store: "~/.reeve/cron/jobs.json",
maxConcurrentRuns: 1 // default 1
}
}
```
Disable cron entirely:
* `cron.enabled: false` (config)
* `REEVE_SKIP_CRON=1` (env)
CLI quickstart [#cli-quickstart]
One-shot reminder (UTC ISO, auto-delete after success):
```bash
reeve cron add \
--name "Send reminder" \
--at "2026-01-12T18:00:00Z" \
--session main \
--system-event "Reminder: submit expense report." \
--wake now \
--delete-after-run
```
One-shot reminder (main session, wake immediately):
```bash
reeve cron add \
--name "Calendar check" \
--at "20m" \
--session main \
--system-event "Next heartbeat: check calendar." \
--wake now
```
Recurring isolated job (deliver to WhatsApp):
```bash
reeve cron add \
--name "Morning status" \
--cron "0 7 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize inbox + calendar for today." \
--deliver \
--channel whatsapp \
--to "+15551234567"
```
Recurring isolated job (deliver to a Telegram topic):
```bash
reeve cron add \
--name "Nightly summary (topic)" \
--cron "0 22 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Summarize today; send to the nightly topic." \
--deliver \
--channel telegram \
--to "-1001234567890:topic:123"
```
Isolated job with model and thinking override:
````bash
reeve cron add \
--name "Deep analysis" \
--cron "0 6 * * 1" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Weekly deep analysis of project progress." \
--model "opus" \
--thinking high \
--deliver \
--channel whatsapp \
--to "+15551234567"
Agent selection (multi-agent setups):
```bash
# Pin a job to agent "ops" (falls back to default if that agent is missing)
reeve cron add --name "Ops sweep" --cron "0 6 * * *" --session isolated --message "Check ops queue" --agent ops
# Switch or clear the agent on an existing job
reeve cron edit --agent ops
reeve cron edit --clear-agent
````
````
Manual run (debug):
```bash
reeve cron run --force
````
Edit an existing job (patch fields):
```bash
reeve cron edit \
--message "Updated prompt" \
--model "opus" \
--thinking low
```
Run history:
```bash
reeve cron runs --id --limit 50
```
Immediate system event without creating a job:
```bash
reeve system event --mode now --text "Next heartbeat: check battery."
```
Gateway API surface [#gateway-api-surface]
* `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove`
* `cron.run` (force or due), `cron.runs`
For immediate system events without a job, use [`reeve system event`](/docs/cli/system).
Troubleshooting [#troubleshooting]
βNothing runsβ [#nothing-runs]
* Check cron is enabled: `cron.enabled` and `REEVE_SKIP_CRON`.
* Check the Gateway is running continuously (cron runs inside the Gateway process).
* For `cron` schedules: confirm timezone (`--tz`) vs the host timezone.
Telegram delivers to the wrong place [#telegram-delivers-to-the-wrong-place]
* For forum topics, use `-100β¦:topic:` so itβs explicit and unambiguous.
* If you see `telegram:...` prefixes in logs or stored βlast routeβ targets, thatβs normal;
cron delivery accepts them and still parses topic IDs correctly.
# Cron vs Heartbeat: When to Use Each (/docs/automation/cron-vs-heartbeat)
Cron vs Heartbeat: When to Use Each [#cron-vs-heartbeat-when-to-use-each]
Both heartbeats and cron jobs let you run tasks on a schedule. This guide helps you choose the right mechanism for your use case.
Quick Decision Guide [#quick-decision-guide]
| Use Case | Recommended | Why |
| ------------------------------------ | ------------------- | ---------------------------------------- |
| Check inbox every 30 min | Heartbeat | Batches with other checks, context-aware |
| Send daily report at 9am sharp | Cron (isolated) | Exact timing needed |
| Monitor calendar for upcoming events | Heartbeat | Natural fit for periodic awareness |
| Run weekly deep analysis | Cron (isolated) | Standalone task, can use different model |
| Remind me in 20 minutes | Cron (main, `--at`) | One-shot with precise timing |
| Background project health check | Heartbeat | Piggybacks on existing cycle |
Heartbeat: Periodic Awareness [#heartbeat-periodic-awareness]
Heartbeats run in the **main session** at a regular interval (default: 30 min). They're designed for the agent to check on things and surface anything important.
When to use heartbeat [#when-to-use-heartbeat]
* **Multiple periodic checks**: Instead of 5 separate cron jobs checking inbox, calendar, weather, notifications, and project status, a single heartbeat can batch all of these.
* **Context-aware decisions**: The agent has full main-session context, so it can make smart decisions about what's urgent vs. what can wait.
* **Conversational continuity**: Heartbeat runs share the same session, so the agent remembers recent conversations and can follow up naturally.
* **Low-overhead monitoring**: One heartbeat replaces many small polling tasks.
Heartbeat advantages [#heartbeat-advantages]
* **Batches multiple checks**: One agent turn can review inbox, calendar, and notifications together.
* **Reduces API calls**: A single heartbeat is cheaper than 5 isolated cron jobs.
* **Context-aware**: The agent knows what you've been working on and can prioritize accordingly.
* **Smart suppression**: If nothing needs attention, the agent replies `HEARTBEAT_OK` and no message is delivered.
* **Natural timing**: Drifts slightly based on queue load, which is fine for most monitoring.
Heartbeat example: HEARTBEAT.md checklist [#heartbeat-example-heartbeatmd-checklist]
```md
# Heartbeat checklist
- Check email for urgent messages
- Review calendar for events in next 2 hours
- If a background task finished, summarize results
- If idle for 8+ hours, send a brief check-in
```
The agent reads this on each heartbeat and handles all items in one turn.
Configuring heartbeat [#configuring-heartbeat]
```json5
{
agents: {
defaults: {
heartbeat: {
every: "30m", // interval
target: "last", // where to deliver alerts
activeHours: { start: "08:00", end: "22:00" } // optional
}
}
}
}
```
See [Heartbeat](/docs/gateway/heartbeat) for full configuration.
Cron: Precise Scheduling [#cron-precise-scheduling]
Cron jobs run at **exact times** and can run in isolated sessions without affecting main context.
When to use cron [#when-to-use-cron]
* **Exact timing required**: "Send this at 9:00 AM every Monday" (not "sometime around 9").
* **Standalone tasks**: Tasks that don't need conversational context.
* **Different model/thinking**: Heavy analysis that warrants a more powerful model.
* **One-shot reminders**: "Remind me in 20 minutes" with `--at`.
* **Noisy/frequent tasks**: Tasks that would clutter main session history.
* **External triggers**: Tasks that should run independently of whether the agent is otherwise active.
Cron advantages [#cron-advantages]
* **Exact timing**: 5-field cron expressions with timezone support.
* **Session isolation**: Runs in `cron:` without polluting main history.
* **Model overrides**: Use a cheaper or more powerful model per job.
* **Delivery control**: Can deliver directly to a channel; still posts a summary to main by default (configurable).
* **No agent context needed**: Runs even if main session is idle or compacted.
* **One-shot support**: `--at` for precise future timestamps.
Cron example: Daily morning briefing [#cron-example-daily-morning-briefing]
```bash
reeve cron add \
--name "Morning briefing" \
--cron "0 7 * * *" \
--tz "America/New_York" \
--session isolated \
--message "Generate today's briefing: weather, calendar, top emails, news summary." \
--model opus \
--deliver \
--channel whatsapp \
--to "+15551234567"
```
This runs at exactly 7:00 AM New York time, uses Opus for quality, and delivers directly to WhatsApp.
Cron example: One-shot reminder [#cron-example-one-shot-reminder]
```bash
reeve cron add \
--name "Meeting reminder" \
--at "20m" \
--session main \
--system-event "Reminder: standup meeting starts in 10 minutes." \
--wake now \
--delete-after-run
```
See [Cron jobs](/docs/automation/cron-jobs) for full CLI reference.
Decision Flowchart [#decision-flowchart]
```
Does the task need to run at an EXACT time?
YES -> Use cron
NO -> Continue...
Does the task need isolation from main session?
YES -> Use cron (isolated)
NO -> Continue...
Can this task be batched with other periodic checks?
YES -> Use heartbeat (add to HEARTBEAT.md)
NO -> Use cron
Is this a one-shot reminder?
YES -> Use cron with --at
NO -> Continue...
Does it need a different model or thinking level?
YES -> Use cron (isolated) with --model/--thinking
NO -> Use heartbeat
```
Combining Both [#combining-both]
The most efficient setup uses **both**:
1. **Heartbeat** handles routine monitoring (inbox, calendar, notifications) in one batched turn every 30 minutes.
2. **Cron** handles precise schedules (daily reports, weekly reviews) and one-shot reminders.
Example: Efficient automation setup [#example-efficient-automation-setup]
**HEARTBEAT.md** (checked every 30 min):
```md
# Heartbeat checklist
- Scan inbox for urgent emails
- Check calendar for events in next 2h
- Review any pending tasks
- Light check-in if quiet for 8+ hours
```
**Cron jobs** (precise timing):
```bash
# Daily morning briefing at 7am
reeve cron add --name "Morning brief" --cron "0 7 * * *" --session isolated --message "..." --deliver
# Weekly project review on Mondays at 9am
reeve cron add --name "Weekly review" --cron "0 9 * * 1" --session isolated --message "..." --model opus
# One-shot reminder
reeve cron add --name "Call back" --at "2h" --session main --system-event "Call back the client" --wake now
```
Lobster: Deterministic workflows with approvals [#lobster-deterministic-workflows-with-approvals]
Lobster is the workflow runtime for **multi-step tool pipelines** that need deterministic execution and explicit approvals.
Use it when the task is more than a single agent turn, and you want a resumable workflow with human checkpoints.
When Lobster fits [#when-lobster-fits]
* **Multi-step automation**: You need a fixed pipeline of tool calls, not a one-off prompt.
* **Approval gates**: Side effects should pause until you approve, then resume.
* **Resumable runs**: Continue a paused workflow without re-running earlier steps.
How it pairs with heartbeat and cron [#how-it-pairs-with-heartbeat-and-cron]
* **Heartbeat/cron** decide *when* a run happens.
* **Lobster** defines *what steps* happen once the run starts.
For scheduled workflows, use cron or heartbeat to trigger an agent turn that calls Lobster.
For ad-hoc workflows, call Lobster directly.
Operational notes (from the code) [#operational-notes-from-the-code]
* Lobster runs as a **local subprocess** (`lobster` CLI) in tool mode and returns a **JSON envelope**.
* If the tool returns `needs_approval`, you resume with a `resumeToken` and `approve` flag.
* The tool is an **optional plugin**; you must allowlist `lobster` in `tools.allow`.
* If you pass `lobsterPath`, it must be an **absolute path**.
See [Lobster](/docs/tools/lobster) for full usage and examples.
Main Session vs Isolated Session [#main-session-vs-isolated-session]
Both heartbeat and cron can interact with the main session, but differently:
| | Heartbeat | Cron (main) | Cron (isolated) |
| ------- | ------------------------------- | ------------------------ | ---------------------- |
| Session | Main | Main (via system event) | `cron:` |
| History | Shared | Shared | Fresh each run |
| Context | Full | Full | None (starts clean) |
| Model | Main session model | Main session model | Can override |
| Output | Delivered if not `HEARTBEAT_OK` | Heartbeat prompt + event | Summary posted to main |
When to use main session cron [#when-to-use-main-session-cron]
Use `--session main` with `--system-event` when you want:
* The reminder/event to appear in main session context
* The agent to handle it during the next heartbeat with full context
* No separate isolated run
```bash
reeve cron add \
--name "Check project" \
--every "4h" \
--session main \
--system-event "Time for a project health check" \
--wake now
```
When to use isolated cron [#when-to-use-isolated-cron]
Use `--session isolated` when you want:
* A clean slate without prior context
* Different model or thinking settings
* Output delivered directly to a channel (summary still posts to main by default)
* History that doesn't clutter main session
```bash
reeve cron add \
--name "Deep analysis" \
--cron "0 6 * * 0" \
--session isolated \
--message "Weekly codebase analysis..." \
--model opus \
--thinking high \
--deliver
```
Cost Considerations [#cost-considerations]
| Mechanism | Cost Profile |
| --------------- | ------------------------------------------------------- |
| Heartbeat | One turn every N minutes; scales with HEARTBEAT.md size |
| Cron (main) | Adds event to next heartbeat (no isolated turn) |
| Cron (isolated) | Full agent turn per job; can use cheaper model |
**Tips**:
* Keep `HEARTBEAT.md` small to minimize token overhead.
* Batch similar checks into heartbeat instead of multiple cron jobs.
* Use `target: "none"` on heartbeat if you only want internal processing.
* Use isolated cron with a cheaper model for routine tasks.
Related [#related]
* [Heartbeat](/docs/gateway/heartbeat) - full heartbeat configuration
* [Cron jobs](/docs/automation/cron-jobs) - full cron CLI and API reference
* [System](/docs/cli/system) - system events + heartbeat controls
# Gmail Pub/Sub -> Reeve (/docs/automation/gmail-pubsub)
Gmail Pub/Sub -> Reeve [#gmail-pubsub---reeve]
Goal: Gmail watch -> Pub/Sub push -> `gog gmail watch serve` -> Reeve webhook.
Prereqs [#prereqs]
* `gcloud` installed and logged in ([install guide](https://docs.cloud.google.com/sdk/docs/install-sdk)).
* `gog` (gogcli) installed and authorized for the Gmail account ([gogcli.sh](https://gogcli.sh/)).
* Reeve hooks enabled (see [Webhooks](/docs/automation/webhook)).
* `tailscale` logged in ([tailscale.com](https://tailscale.com/)). Supported setup uses Tailscale Funnel for the public HTTPS endpoint.
Other tunnel services can work, but are DIY/unsupported and require manual wiring.
Right now, Tailscale is what we support.
Example hook config (enable Gmail preset mapping):
```json5
{
hooks: {
enabled: true,
token: "REEVE_HOOK_TOKEN",
path: "/hooks",
presets: ["gmail"]
}
}
```
To deliver the Gmail summary to a chat surface, override the preset with a mapping
that sets `deliver` + optional `channel`/`to`:
```json5
{
hooks: {
enabled: true,
token: "REEVE_HOOK_TOKEN",
presets: ["gmail"],
mappings: [
{
match: { path: "gmail" },
action: "agent",
wakeMode: "now",
name: "Gmail",
sessionKey: "hook:gmail:{{messages[0].id}}",
messageTemplate:
"New email from {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}\n{{messages[0].body}}",
model: "openai/gpt-5.2-mini",
deliver: true,
channel: "last"
// to: "+15551234567"
}
]
}
}
```
If you want a fixed channel, set `channel` + `to`. Otherwise `channel: "last"`
uses the last delivery route (falls back to WhatsApp).
To force a cheaper model for Gmail runs, set `model` in the mapping
(`provider/model` or alias). If you enforce `agents.defaults.models`, include it there.
To set a default model and thinking level specifically for Gmail hooks, add
`hooks.gmail.model` / `hooks.gmail.thinking` in your config:
```json5
{
hooks: {
gmail: {
model: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
thinking: "off"
}
}
}
```
Notes:
* Per-hook `model`/`thinking` in the mapping still overrides these defaults.
* Fallback order: `hooks.gmail.model` β `agents.defaults.model.fallbacks` β primary (auth/rate-limit/timeouts).
* If `agents.defaults.models` is set, the Gmail model must be in the allowlist.
To customize payload handling further, add `hooks.mappings` or a JS/TS transform module
under `hooks.transformsDir` (see [Webhooks](/docs/automation/webhook)).
Wizard (recommended) [#wizard-recommended]
Use the Reeve helper to wire everything together (installs deps on macOS via brew):
```bash
reeve webhooks gmail setup \
--account reeve@gmail.com
```
Defaults:
* Uses Tailscale Funnel for the public push endpoint.
* Writes `hooks.gmail` config for `reeve webhooks gmail run`.
* Enables the Gmail hook preset (`hooks.presets: ["gmail"]`).
Path note: when `tailscale.mode` is enabled, Reeve automatically sets
`hooks.gmail.serve.path` to `/` and keeps the public path at
`hooks.gmail.tailscale.path` (default `/gmail-pubsub`) because Tailscale
strips the set-path prefix before proxying.
If you need the backend to receive the prefixed path, set
`hooks.gmail.tailscale.target` (or `--tailscale-target`) to a full URL like
`http://127.0.0.1:8788/gmail-pubsub` and match `hooks.gmail.serve.path`.
Want a custom endpoint? Use `--push-endpoint ` or `--tailscale off`.
Platform note: on macOS the wizard installs `gcloud`, `gogcli`, and `tailscale`
via Homebrew; on Linux install them manually first.
Gateway auto-start (recommended):
* When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts
`gog gmail watch serve` on boot and auto-renews the watch.
* Set `REEVE_SKIP_GMAIL_WATCHER=1` to opt out (useful if you run the daemon yourself).
* Do not run the manual daemon at the same time, or you will hit
`listen tcp 127.0.0.1:8788: bind: address already in use`.
Manual daemon (starts `gog gmail watch serve` + auto-renew):
```bash
reeve webhooks gmail run
```
One-time setup [#one-time-setup]
1. Select the GCP project **that owns the OAuth client** used by `gog`.
```bash
gcloud auth login
gcloud config set project
```
Note: Gmail watch requires the Pub/Sub topic to live in the same project as the OAuth client.
2. Enable APIs:
```bash
gcloud services enable gmail.googleapis.com pubsub.googleapis.com
```
3. Create a topic:
```bash
gcloud pubsub topics create gog-gmail-watch
```
4. Allow Gmail push to publish:
```bash
gcloud pubsub topics add-iam-policy-binding gog-gmail-watch \
--member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
--role=roles/pubsub.publisher
```
Start the watch [#start-the-watch]
```bash
gog gmail watch start \
--account reeve@gmail.com \
--label INBOX \
--topic projects//topics/gog-gmail-watch
```
Save the `history_id` from the output (for debugging).
Run the push handler [#run-the-push-handler]
Local example (shared token auth):
```bash
gog gmail watch serve \
--account reeve@gmail.com \
--bind 127.0.0.1 \
--port 8788 \
--path /gmail-pubsub \
--token \
--hook-url http://127.0.0.1:18789/hooks/gmail \
--hook-token REEVE_HOOK_TOKEN \
--include-body \
--max-bytes 20000
```
Notes:
* `--token` protects the push endpoint (`x-gog-token` or `?token=`).
* `--hook-url` points to Reeve `/hooks/gmail` (mapped; isolated run + summary to main).
* `--include-body` and `--max-bytes` control the body snippet sent to Reeve.
Recommended: `reeve webhooks gmail run` wraps the same flow and auto-renews the watch.
Expose the handler (advanced, unsupported) [#expose-the-handler-advanced-unsupported]
If you need a non-Tailscale tunnel, wire it manually and use the public URL in the push
subscription (unsupported, no guardrails):
```bash
cloudflared tunnel --url http://127.0.0.1:8788 --no-autoupdate
```
Use the generated URL as the push endpoint:
```bash
gcloud pubsub subscriptions create gog-gmail-watch-push \
--topic gog-gmail-watch \
--push-endpoint "https:///gmail-pubsub?token="
```
Production: use a stable HTTPS endpoint and configure Pub/Sub OIDC JWT, then run:
```bash
gog gmail watch serve --verify-oidc --oidc-email
```
Test [#test]
Send a message to the watched inbox:
```bash
gog gmail send \
--account reeve@gmail.com \
--to reeve@gmail.com \
--subject "watch test" \
--body "ping"
```
Check watch state and history:
```bash
gog gmail watch status --account reeve@gmail.com
gog gmail history --account reeve@gmail.com --since
```
Troubleshooting [#troubleshooting]
* `Invalid topicName`: project mismatch (topic not in the OAuth client project).
* `User not authorized`: missing `roles/pubsub.publisher` on the topic.
* Empty messages: Gmail push only provides `historyId`; fetch via `gog gmail history`.
Cleanup [#cleanup]
```bash
gog gmail watch stop --account reeve@gmail.com
gcloud pubsub subscriptions delete gog-gmail-watch-push
gcloud pubsub topics delete gog-gmail-watch
```
# Hooks (/docs/automation/hooks)
Hooks [#hooks]
Hooks provide an extensible event-driven system for automating actions in response to agent commands and events. Hooks are automatically discovered from directories and can be managed via CLI commands, similar to how skills work in Reeve.
Getting Oriented [#getting-oriented]
Hooks are small scripts that run when something happens. There are two kinds:
* **Hooks** (this page): run inside the Gateway when agent events fire, like `/new`, `/reset`, `/stop`, or lifecycle events.
* **Webhooks**: external HTTP webhooks that let other systems trigger work in Reeve. See [Webhook Hooks](/docs/automation/webhook) or use `reeve webhooks` for Gmail helper commands.
Hooks can also be bundled inside plugins; see [Plugins](/plugin#plugin-hooks).
Common uses:
* Save a memory snapshot when you reset a session
* Keep an audit trail of commands for troubleshooting or compliance
* Trigger follow-up automation when a session starts or ends
* Write files into the agent workspace or call external APIs when events fire
If you can write a small TypeScript function, you can write a hook. Hooks are discovered automatically, and you enable or disable them via the CLI.
Overview [#overview]
The hooks system allows you to:
* Save session context to memory when `/new` is issued
* Log all commands for auditing
* Trigger custom automations on agent lifecycle events
* Extend Reeve's behavior without modifying core code
Getting Started [#getting-started]
Bundled Hooks [#bundled-hooks]
Reeve ships with four bundled hooks that are automatically discovered:
* **πΎ session-memory**: Saves session context to your agent workspace (default `~/reeve/memory/`) when you issue `/new`
* **π command-logger**: Logs all command events to `~/.reeve/logs/commands.log`
* **π boot-md**: Runs `BOOT.md` when the gateway starts (requires internal hooks enabled)
* **π soul-evil**: Swaps injected `SOUL.md` content with `SOUL_EVIL.md` during a purge window or by random chance
List available hooks:
```bash
reeve hooks list
```
Enable a hook:
```bash
reeve hooks enable session-memory
```
Check hook status:
```bash
reeve hooks check
```
Get detailed information:
```bash
reeve hooks info session-memory
```
Onboarding [#onboarding]
During onboarding (`reeve onboard`), you'll be prompted to enable recommended hooks. The wizard automatically discovers eligible hooks and presents them for selection.
Hook Discovery [#hook-discovery]
Hooks are automatically discovered from three directories (in order of precedence):
1. **Workspace hooks**: `/hooks/` (per-agent, highest precedence)
2. **Managed hooks**: `~/.reeve/hooks/` (user-installed, shared across workspaces)
3. **Bundled hooks**: `/dist/hooks/bundled/` (shipped with Reeve)
Managed hook directories can be either a **single hook** or a **hook pack** (package directory).
Each hook is a directory containing:
```
my-hook/
βββ HOOK.md # Metadata + documentation
βββ handler.ts # Handler implementation
```
Hook Packs (npm/archives) [#hook-packs-npmarchives]
Hook packs are standard npm packages that export one or more hooks via `reeve.hooks` in
`package.json`. Install them with:
```bash
reeve hooks install
```
Example `package.json`:
```json
{
"name": "@acme/my-hooks",
"version": "0.1.0",
"reeve": {
"hooks": ["./hooks/my-hook", "./hooks/other-hook"]
}
}
```
Each entry points to a hook directory containing `HOOK.md` and `handler.ts` (or `index.ts`).
Hook packs can ship dependencies; they will be installed under `~/.reeve/hooks/`.
Hook Structure [#hook-structure]
HOOK.md Format [#hookmd-format]
The `HOOK.md` file contains metadata in YAML frontmatter plus Markdown documentation:
```markdown
---
name: my-hook
description: "Short description of what this hook does"
homepage: https://docs.reeve.com/hooks#my-hook
metadata: {"reeve":{"emoji":"π","events":["command:new"],"requires":{"bins":["node"]}}}
---
# My Hook
Detailed documentation goes here...
## What It Does
- Listens for `/new` commands
- Performs some action
- Logs the result
## Requirements
- Node.js must be installed
## Configuration
No configuration needed.
```
Metadata Fields [#metadata-fields]
The `metadata.reeve` object supports:
* **`emoji`**: Display emoji for CLI (e.g., `"πΎ"`)
* **`events`**: Array of events to listen for (e.g., `["command:new", "command:reset"]`)
* **`export`**: Named export to use (defaults to `"default"`)
* **`homepage`**: Documentation URL
* **`requires`**: Optional requirements
* **`bins`**: Required binaries on PATH (e.g., `["git", "node"]`)
* **`anyBins`**: At least one of these binaries must be present
* **`env`**: Required environment variables
* **`config`**: Required config paths (e.g., `["workspace.dir"]`)
* **`os`**: Required platforms (e.g., `["darwin", "linux"]`)
* **`always`**: Bypass eligibility checks (boolean)
* **`install`**: Installation methods (for bundled hooks: `[{"id":"bundled","kind":"bundled"}]`)
Handler Implementation [#handler-implementation]
The `handler.ts` file exports a `HookHandler` function:
```typescript
const myHandler: HookHandler = async (event) => {
// Only trigger on 'new' command
if (event.type !== 'command' || event.action !== 'new') {
return;
}
console.log(`[my-hook] New command triggered`);
console.log(` Session: ${event.sessionKey}`);
console.log(` Timestamp: ${event.timestamp.toISOString()}`);
// Your custom logic here
// Optionally send message to user
event.messages.push('β¨ My hook executed!');
};
export default myHandler;
```
Event Context [#event-context]
Each event includes:
```typescript
{
type: 'command' | 'session' | 'agent' | 'gateway',
action: string, // e.g., 'new', 'reset', 'stop'
sessionKey: string, // Session identifier
timestamp: Date, // When the event occurred
messages: string[], // Push messages here to send to user
context: {
sessionEntry?: SessionEntry,
sessionId?: string,
sessionFile?: string,
commandSource?: string, // e.g., 'whatsapp', 'telegram'
senderId?: string,
workspaceDir?: string,
bootstrapFiles?: WorkspaceBootstrapFile[],
cfg?: ReeveConfig
}
}
```
Event Types [#event-types]
Command Events [#command-events]
Triggered when agent commands are issued:
* **`command`**: All command events (general listener)
* **`command:new`**: When `/new` command is issued
* **`command:reset`**: When `/reset` command is issued
* **`command:stop`**: When `/stop` command is issued
Agent Events [#agent-events]
* **`agent:bootstrap`**: Before workspace bootstrap files are injected (hooks may mutate `context.bootstrapFiles`)
Gateway Events [#gateway-events]
Triggered when the gateway starts:
* **`gateway:startup`**: After channels start and hooks are loaded
Tool Result Hooks (Plugin API) [#tool-result-hooks-plugin-api]
These hooks are not event-stream listeners; they let plugins synchronously adjust tool results before Reeve persists them.
* **`tool_result_persist`**: transform tool results before they are written to the session transcript. Must be synchronous; return the updated tool result payload or `undefined` to keep it as-is. See [Agent Loop](/docs/concepts/agent-loop).
Future Events [#future-events]
Planned event types:
* **`session:start`**: When a new session begins
* **`session:end`**: When a session ends
* **`agent:error`**: When an agent encounters an error
* **`message:sent`**: When a message is sent
* **`message:received`**: When a message is received
Creating Custom Hooks [#creating-custom-hooks]
1. Choose Location [#1-choose-location]
* **Workspace hooks** (`/hooks/`): Per-agent, highest precedence
* **Managed hooks** (`~/.reeve/hooks/`): Shared across workspaces
2. Create Directory Structure [#2-create-directory-structure]
```bash
mkdir -p ~/.reeve/hooks/my-hook
cd ~/.reeve/hooks/my-hook
```
3. Create HOOK.md [#3-create-hookmd]
```markdown
---
name: my-hook
description: "Does something useful"
metadata: {"reeve":{"emoji":"π―","events":["command:new"]}}
---
# My Custom Hook
This hook does something useful when you issue `/new`.
```
4. Create handler.ts [#4-create-handlerts]
```typescript
const handler: HookHandler = async (event) => {
if (event.type !== 'command' || event.action !== 'new') {
return;
}
console.log('[my-hook] Running!');
// Your logic here
};
export default handler;
```
5. Enable and Test [#5-enable-and-test]
```bash
# Verify hook is discovered
reeve hooks list
# Enable it
reeve hooks enable my-hook
# Restart your gateway process (menu bar app restart on macOS, or restart your dev process)
# Trigger the event
# Send /new via your messaging channel
```
Configuration [#configuration]
New Config Format (Recommended) [#new-config-format-recommended]
```json
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"session-memory": { "enabled": true },
"command-logger": { "enabled": false }
}
}
}
}
```
Per-Hook Configuration [#per-hook-configuration]
Hooks can have custom configuration:
```json
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"my-hook": {
"enabled": true,
"env": {
"MY_CUSTOM_VAR": "value"
}
}
}
}
}
}
```
Extra Directories [#extra-directories]
Load hooks from additional directories:
```json
{
"hooks": {
"internal": {
"enabled": true,
"load": {
"extraDirs": ["/path/to/more/hooks"]
}
}
}
}
```
Legacy Config Format (Still Supported) [#legacy-config-format-still-supported]
The old config format still works for backwards compatibility:
```json
{
"hooks": {
"internal": {
"enabled": true,
"handlers": [
{
"event": "command:new",
"module": "./hooks/handlers/my-handler.ts",
"export": "default"
}
]
}
}
}
```
**Migration**: Use the new discovery-based system for new hooks. Legacy handlers are loaded after directory-based hooks.
CLI Commands [#cli-commands]
List Hooks [#list-hooks]
```bash
# List all hooks
reeve hooks list
# Show only eligible hooks
reeve hooks list --eligible
# Verbose output (show missing requirements)
reeve hooks list --verbose
# JSON output
reeve hooks list --json
```
Hook Information [#hook-information]
```bash
# Show detailed info about a hook
reeve hooks info session-memory
# JSON output
reeve hooks info session-memory --json
```
Check Eligibility [#check-eligibility]
```bash
# Show eligibility summary
reeve hooks check
# JSON output
reeve hooks check --json
```
Enable/Disable [#enabledisable]
```bash
# Enable a hook
reeve hooks enable session-memory
# Disable a hook
reeve hooks disable command-logger
```
Bundled Hooks [#bundled-hooks-1]
session-memory [#session-memory]
Saves session context to memory when you issue `/new`.
**Events**: `command:new`
**Requirements**: `workspace.dir` must be configured
**Output**: `/memory/YYYY-MM-DD-slug.md` (defaults to `~/reeve`)
**What it does**:
1. Uses the pre-reset session entry to locate the correct transcript
2. Extracts the last 15 lines of conversation
3. Uses LLM to generate a descriptive filename slug
4. Saves session metadata to a dated memory file
**Example output**:
```markdown
# Session: 2026-01-16 14:30:00 UTC
- **Session Key**: agent:main:main
- **Session ID**: abc123def456
- **Source**: telegram
```
**Filename examples**:
* `2026-01-16-vendor-pitch.md`
* `2026-01-16-api-design.md`
* `2026-01-16-1430.md` (fallback timestamp if slug generation fails)
**Enable**:
```bash
reeve hooks enable session-memory
```
command-logger [#command-logger]
Logs all command events to a centralized audit file.
**Events**: `command`
**Requirements**: None
**Output**: `~/.reeve/logs/commands.log`
**What it does**:
1. Captures event details (command action, timestamp, session key, sender ID, source)
2. Appends to log file in JSONL format
3. Runs silently in the background
**Example log entries**:
```jsonl
{"timestamp":"2026-01-16T14:30:00.000Z","action":"new","sessionKey":"agent:main:main","senderId":"+1234567890","source":"telegram"}
{"timestamp":"2026-01-16T15:45:22.000Z","action":"stop","sessionKey":"agent:main:main","senderId":"user@example.com","source":"whatsapp"}
```
**View logs**:
```bash
# View recent commands
tail -n 20 ~/.reeve/logs/commands.log
# Pretty-print with jq
cat ~/.reeve/logs/commands.log | jq .
# Filter by action
grep '"action":"new"' ~/.reeve/logs/commands.log | jq .
```
**Enable**:
```bash
reeve hooks enable command-logger
```
soul-evil [#soul-evil]
Swaps injected `SOUL.md` content with `SOUL_EVIL.md` during a purge window or by random chance.
**Events**: `agent:bootstrap`
**Docs**: [SOUL Evil Hook](/docs/hooks/soul-evil)
**Output**: No files written; swaps happen in-memory only.
**Enable**:
```bash
reeve hooks enable soul-evil
```
**Config**:
```json
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"soul-evil": {
"enabled": true,
"file": "SOUL_EVIL.md",
"chance": 0.1,
"purge": { "at": "21:00", "duration": "15m" }
}
}
}
}
}
```
boot-md [#boot-md]
Runs `BOOT.md` when the gateway starts (after channels start).
Internal hooks must be enabled for this to run.
**Events**: `gateway:startup`
**Requirements**: `workspace.dir` must be configured
**What it does**:
1. Reads `BOOT.md` from your workspace
2. Runs the instructions via the agent runner
3. Sends any requested outbound messages via the message tool
**Enable**:
```bash
reeve hooks enable boot-md
```
Best Practices [#best-practices]
Keep Handlers Fast [#keep-handlers-fast]
Hooks run during command processing. Keep them lightweight:
```typescript
// β Good - async work, returns immediately
const handler: HookHandler = async (event) => {
void processInBackground(event); // Fire and forget
};
// β Bad - blocks command processing
const handler: HookHandler = async (event) => {
await slowDatabaseQuery(event);
await evenSlowerAPICall(event);
};
```
Handle Errors Gracefully [#handle-errors-gracefully]
Always wrap risky operations:
```typescript
const handler: HookHandler = async (event) => {
try {
await riskyOperation(event);
} catch (err) {
console.error('[my-handler] Failed:', err instanceof Error ? err.message : String(err));
// Don't throw - let other handlers run
}
};
```
Filter Events Early [#filter-events-early]
Return early if the event isn't relevant:
```typescript
const handler: HookHandler = async (event) => {
// Only handle 'new' commands
if (event.type !== 'command' || event.action !== 'new') {
return;
}
// Your logic here
};
```
Use Specific Event Keys [#use-specific-event-keys]
Specify exact events in metadata when possible:
```yaml
metadata: {"reeve":{"events":["command:new"]}} # Specific
```
Rather than:
```yaml
metadata: {"reeve":{"events":["command"]}} # General - more overhead
```
Debugging [#debugging]
Enable Hook Logging [#enable-hook-logging]
The gateway logs hook loading at startup:
```
Registered hook: session-memory -> command:new
Registered hook: command-logger -> command
Registered hook: boot-md -> gateway:startup
```
Check Discovery [#check-discovery]
List all discovered hooks:
```bash
reeve hooks list --verbose
```
Check Registration [#check-registration]
In your handler, log when it's called:
```typescript
const handler: HookHandler = async (event) => {
console.log('[my-handler] Triggered:', event.type, event.action);
// Your logic
};
```
Verify Eligibility [#verify-eligibility]
Check why a hook isn't eligible:
```bash
reeve hooks info my-hook
```
Look for missing requirements in the output.
Testing [#testing]
Gateway Logs [#gateway-logs]
Monitor gateway logs to see hook execution:
```bash
# macOS
./scripts/clawlog.sh -f
# Other platforms
tail -f ~/.reeve/gateway.log
```
Test Hooks Directly [#test-hooks-directly]
Test your handlers in isolation:
```typescript
test('my handler works', async () => {
const event = createHookEvent('command', 'new', 'test-session', {
foo: 'bar'
});
await myHandler(event);
// Assert side effects
});
```
Architecture [#architecture]
Core Components [#core-components]
* **`src/hooks/types.ts`**: Type definitions
* **`src/hooks/workspace.ts`**: Directory scanning and loading
* **`src/hooks/frontmatter.ts`**: HOOK.md metadata parsing
* **`src/hooks/config.ts`**: Eligibility checking
* **`src/hooks/hooks-status.ts`**: Status reporting
* **`src/hooks/loader.ts`**: Dynamic module loader
* **`src/cli/hooks-cli.ts`**: CLI commands
* **`src/gateway/server-startup.ts`**: Loads hooks at gateway start
* **`src/auto-reply/reply/commands-core.ts`**: Triggers command events
Discovery Flow [#discovery-flow]
```
Gateway startup
β
Scan directories (workspace β managed β bundled)
β
Parse HOOK.md files
β
Check eligibility (bins, env, config, os)
β
Load handlers from eligible hooks
β
Register handlers for events
```
Event Flow [#event-flow]
```
User sends /new
β
Command validation
β
Create hook event
β
Trigger hook (all registered handlers)
β
Command processing continues
β
Session reset
```
Troubleshooting [#troubleshooting]
Hook Not Discovered [#hook-not-discovered]
1. Check directory structure:
```bash
ls -la ~/.reeve/hooks/my-hook/
# Should show: HOOK.md, handler.ts
```
2. Verify HOOK.md format:
```bash
cat ~/.reeve/hooks/my-hook/HOOK.md
# Should have YAML frontmatter with name and metadata
```
3. List all discovered hooks:
```bash
reeve hooks list
```
Hook Not Eligible [#hook-not-eligible]
Check requirements:
```bash
reeve hooks info my-hook
```
Look for missing:
* Binaries (check PATH)
* Environment variables
* Config values
* OS compatibility
Hook Not Executing [#hook-not-executing]
1. Verify hook is enabled:
```bash
reeve hooks list
# Should show β next to enabled hooks
```
2. Restart your gateway process so hooks reload.
3. Check gateway logs for errors:
```bash
./scripts/clawlog.sh | grep hook
```
Handler Errors [#handler-errors]
Check for TypeScript/import errors:
```bash
# Test import directly
node -e "import('./path/to/handler.ts').then(console.log)"
```
Migration Guide [#migration-guide]
From Legacy Config to Discovery [#from-legacy-config-to-discovery]
**Before**:
```json
{
"hooks": {
"internal": {
"enabled": true,
"handlers": [
{
"event": "command:new",
"module": "./hooks/handlers/my-handler.ts"
}
]
}
}
}
```
**After**:
1. Create hook directory:
```bash
mkdir -p ~/.reeve/hooks/my-hook
mv ./hooks/handlers/my-handler.ts ~/.reeve/hooks/my-hook/handler.ts
```
2. Create HOOK.md:
```markdown
---
name: my-hook
description: "My custom hook"
metadata: {"reeve":{"emoji":"π―","events":["command:new"]}}
---
# My Hook
Does something useful.
```
3. Update config:
```json
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"my-hook": { "enabled": true }
}
}
}
}
```
4. Verify and restart your gateway process:
```bash
reeve hooks list
# Should show: π― my-hook β
```
**Benefits of migration**:
* Automatic discovery
* CLI management
* Eligibility checking
* Better documentation
* Consistent structure
See Also [#see-also]
* [CLI Reference: hooks](/docs/cli/hooks)
* [Bundled Hooks README](https://github.com/MindFortressInc/reeve/tree/main/src/hooks/bundled)
* [Webhook Hooks](/docs/automation/webhook)
* [Configuration](/docs/gateway/configuration#hooks)
# Polls (/docs/automation/poll)
Polls [#polls]
Supported channels [#supported-channels]
* WhatsApp (web channel)
* Discord
* MS Teams (Adaptive Cards)
CLI [#cli]
```bash
# WhatsApp
reeve message poll --target +15555550123 \
--poll-question "Lunch today?" --poll-option "Yes" --poll-option "No" --poll-option "Maybe"
reeve message poll --target 123456789@g.us \
--poll-question "Meeting time?" --poll-option "10am" --poll-option "2pm" --poll-option "4pm" --poll-multi
# Discord
reeve message poll --channel discord --target channel:123456789 \
--poll-question "Snack?" --poll-option "Pizza" --poll-option "Sushi"
reeve message poll --channel discord --target channel:123456789 \
--poll-question "Plan?" --poll-option "A" --poll-option "B" --poll-duration-hours 48
# MS Teams
reeve message poll --channel msteams --target conversation:19:abc@thread.tacv2 \
--poll-question "Lunch?" --poll-option "Pizza" --poll-option "Sushi"
```
Options:
* `--channel`: `whatsapp` (default), `discord`, or `msteams`
* `--poll-multi`: allow selecting multiple options
* `--poll-duration-hours`: Discord-only (defaults to 24 when omitted)
Gateway RPC [#gateway-rpc]
Method: `poll`
Params:
* `to` (string, required)
* `question` (string, required)
* `options` (string\[], required)
* `maxSelections` (number, optional)
* `durationHours` (number, optional)
* `channel` (string, optional, default: `whatsapp`)
* `idempotencyKey` (string, required)
Channel differences [#channel-differences]
* WhatsApp: 2-12 options, `maxSelections` must be within option count, ignores `durationHours`.
* Discord: 2-10 options, `durationHours` clamped to 1-768 hours (default 24). `maxSelections > 1` enables multi-select; Discord does not support a strict selection count.
* MS Teams: Adaptive Card polls (Reeve-managed). No native poll API; `durationHours` is ignored.
Agent tool (Message) [#agent-tool-message]
Use the `message` tool with `poll` action (`to`, `pollQuestion`, `pollOption`, optional `pollMulti`, `pollDurationHours`, `channel`).
Note: Discord has no βpick exactly Nβ mode; `pollMulti` maps to multi-select.
Teams polls are rendered as Adaptive Cards and require the gateway to stay online
to record votes in `~/.reeve/msteams-polls.json`.
# Webhooks (/docs/automation/webhook)
Webhooks [#webhooks]
Gateway can expose a small HTTP webhook endpoint for external triggers.
Enable [#enable]
```json5
{
hooks: {
enabled: true,
token: "shared-secret",
path: "/hooks"
}
}
```
Notes:
* `hooks.token` is required when `hooks.enabled=true`.
* `hooks.path` defaults to `/hooks`.
Auth [#auth]
Every request must include the hook token:
* `Authorization: Bearer `
* or `x-reeve-token: `
* or `?token=`
Endpoints [#endpoints]
POST /hooks/wake [#post-hookswake]
Payload:
```json
{ "text": "System line", "mode": "now" }
```
* `text` **required** (string): The description of the event (e.g., "New email received").
* `mode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
Effect:
* Enqueues a system event for the **main** session
* If `mode=now`, triggers an immediate heartbeat
POST /hooks/agent [#post-hooksagent]
Payload:
```json
{
"message": "Run this",
"name": "Email",
"sessionKey": "hook:email:msg-123",
"wakeMode": "now",
"deliver": true,
"channel": "last",
"to": "+15551234567",
"model": "openai/gpt-5.2-mini",
"thinking": "low",
"timeoutSeconds": 120
}
```
* `message` **required** (string): The prompt or message for the agent to process.
* `name` optional (string): Human-readable name for the hook (e.g., "GitHub"), used as a prefix in session summaries.
* `sessionKey` optional (string): The key used to identify the agent's session. Defaults to a random `hook:`. Using a consistent key allows for a multi-turn conversation within the hook context.
* `wakeMode` optional (`now` | `next-heartbeat`): Whether to trigger an immediate heartbeat (default `now`) or wait for the next periodic check.
* `deliver` optional (boolean): If `true`, the agent's response will be sent to the messaging channel. Defaults to `true`. Responses that are only heartbeat acknowledgments are automatically skipped.
* `channel` optional (string): The messaging channel for delivery. One of: `last`, `whatsapp`, `telegram`, `discord`, `slack`, `mattermost` (plugin), `signal`, `imessage`, `msteams`. Defaults to `last`.
* `to` optional (string): The recipient identifier for the channel (e.g., phone number for WhatsApp/Signal, chat ID for Telegram, channel ID for Discord/Slack/Mattermost (plugin), conversation ID for MS Teams). Defaults to the last recipient in the main session.
* `model` optional (string): Model override (e.g., `anthropic/claude-3-5-sonnet` or an alias). Must be in the allowed model list if restricted.
* `thinking` optional (string): Thinking level override (e.g., `low`, `medium`, `high`).
* `timeoutSeconds` optional (number): Maximum duration for the agent run in seconds.
Effect:
* Runs an **isolated** agent turn (own session key)
* Always posts a summary into the **main** session
* If `wakeMode=now`, triggers an immediate heartbeat
POST /hooks/ (mapped) [#post-hooksname-mapped]
Custom hook names are resolved via `hooks.mappings` (see configuration). A mapping can
turn arbitrary payloads into `wake` or `agent` actions, with optional templates or
code transforms.
Mapping options (summary):
* `hooks.presets: ["gmail"]` enables the built-in Gmail mapping.
* `hooks.mappings` lets you define `match`, `action`, and templates in config.
* `hooks.transformsDir` + `transform.module` loads a JS/TS module for custom logic.
* Use `match.source` to keep a generic ingest endpoint (payload-driven routing).
* TS transforms require a TS loader (e.g. `bun` or `tsx`) or precompiled `.js` at runtime.
* Set `deliver: true` + `channel`/`to` on mappings to route replies to a chat surface
(`channel` defaults to `last` and falls back to WhatsApp).
* `reeve webhooks gmail setup` writes `hooks.gmail` config for `reeve webhooks gmail run`.
See [Gmail Pub/Sub](/docs/automation/gmail-pubsub) for the full Gmail watch flow.
Responses [#responses]
* `200` for `/hooks/wake`
* `202` for `/hooks/agent` (async run started)
* `401` on auth failure
* `400` on invalid payload
* `413` on oversized payloads
Examples [#examples]
```bash
curl -X POST http://127.0.0.1:18789/hooks/wake \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"text":"New email received","mode":"now"}'
```
```bash
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'x-reeve-token: SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","wakeMode":"next-heartbeat"}'
```
Use a different model [#use-a-different-model]
Add `model` to the agent payload (or mapping) to override the model for that run:
```bash
curl -X POST http://127.0.0.1:18789/hooks/agent \
-H 'x-reeve-token: SECRET' \
-H 'Content-Type: application/json' \
-d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.2-mini"}'
```
If you enforce `agents.defaults.models`, make sure the override model is included there.
```bash
curl -X POST http://127.0.0.1:18789/hooks/gmail \
-H 'Authorization: Bearer SECRET' \
-H 'Content-Type: application/json' \
-d '{"source":"gmail","messages":[{"from":"Ada","subject":"Hello","snippet":"Hi"}]}'
```
Security [#security]
* Keep hook endpoints behind loopback, tailnet, or trusted reverse proxy.
* Use a dedicated hook token; do not reuse gateway auth tokens.
* Avoid including sensitive raw payloads in webhook logs.
# BlueBubbles (macOS REST) (/docs/channels/bluebubbles)
BlueBubbles (macOS REST) [#bluebubbles-macos-rest]
Status: bundled plugin that talks to the BlueBubbles macOS server over HTTP. **Recommended for iMessage integration** due to its richer API and easier setup compared to the legacy imsg channel.
Overview [#overview]
* Runs on macOS via the BlueBubbles helper app ([bluebubbles.app](https://bluebubbles.app)).
* Recommended/tested: macOS Sequoia (15). macOS Tahoe (26) works; edit is currently broken on Tahoe, and group icon updates may report success but not sync.
* Reeve talks to it through its REST API (`GET /api/v1/ping`, `POST /message/text`, `POST /chat/:id/*`).
* Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls.
* Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible).
* Pairing/allowlist works the same way as other channels (`/start/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes.
* Reactions are surfaced as system events just like Slack/Telegram so agents can "mention" them before replying.
* Advanced features: edit, unsend, reply threading, message effects, group management.
Quick start [#quick-start]
1. Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)).
2. In the BlueBubbles config, enable the web API and set a password.
3. Run `reeve onboard` and select BlueBubbles, or configure manually:
```json5
{
channels: {
bluebubbles: {
enabled: true,
serverUrl: "http://192.168.1.100:1234",
password: "example-password",
webhookPath: "/bluebubbles-webhook"
}
}
}
```
4. Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=`).
5. Start the gateway; it will register the webhook handler and start pairing.
Onboarding [#onboarding]
BlueBubbles is available in the interactive setup wizard:
```
reeve onboard
```
The wizard prompts for:
* **Server URL** (required): BlueBubbles server address (e.g., `http://192.168.1.100:1234`)
* **Password** (required): API password from BlueBubbles Server settings
* **Webhook path** (optional): Defaults to `/bluebubbles-webhook`
* **DM policy**: pairing, allowlist, open, or disabled
* **Allow list**: Phone numbers, emails, or chat targets
You can also add BlueBubbles via CLI:
```
reeve channels add bluebubbles --http-url http://192.168.1.100:1234 --password
```
Access control (DMs + groups) [#access-control-dms--groups]
DMs:
* Default: `channels.bluebubbles.dmPolicy = "pairing"`.
* Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
* Approve via:
* `reeve pairing list bluebubbles`
* `reeve pairing approve bluebubbles `
* Pairing is the default token exchange. Details: [Pairing](/docs/start/pairing)
Groups:
* `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
* `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
Mention gating (groups) [#mention-gating-groups]
BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior:
* Uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) to detect mentions.
* When `requireMention` is enabled for a group, the agent only responds when mentioned.
* Control commands from authorized senders bypass mention gating.
Per-group configuration:
```json5
{
channels: {
bluebubbles: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123"],
groups: {
"*": { requireMention: true }, // default for all groups
"iMessage;-;chat123": { requireMention: false } // override for specific group
}
}
}
}
```
Command gating [#command-gating]
* Control commands (e.g., `/config`, `/model`) require authorization.
* Uses `allowFrom` and `groupAllowFrom` to determine command authorization.
* Authorized senders can run control commands even without mentioning in groups.
Typing + read receipts [#typing--read-receipts]
* **Typing indicators**: Sent automatically before and during response generation.
* **Read receipts**: Controlled by `channels.bluebubbles.sendReadReceipts` (default: `true`).
* **Typing indicators**: Reeve sends typing start events; BlueBubbles clears typing automatically on send or timeout (manual stop via DELETE is unreliable).
```json5
{
channels: {
bluebubbles: {
sendReadReceipts: false // disable read receipts
}
}
}
```
Advanced actions [#advanced-actions]
BlueBubbles supports advanced message actions when enabled in config:
```json5
{
channels: {
bluebubbles: {
actions: {
reactions: true, // tapbacks (default: true)
edit: true, // edit sent messages (macOS 13+, broken on macOS 26 Tahoe)
unsend: true, // unsend messages (macOS 13+)
reply: true, // reply threading by message GUID
sendWithEffect: true, // message effects (slam, loud, etc.)
renameGroup: true, // rename group chats
setGroupIcon: true, // set group chat icon/photo (flaky on macOS 26 Tahoe)
addParticipant: true, // add participants to groups
removeParticipant: true, // remove participants from groups
leaveGroup: true, // leave group chats
sendAttachment: true // send attachments/media
}
}
}
}
```
Available actions:
* **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`)
* **edit**: Edit a sent message (`messageId`, `text`)
* **unsend**: Unsend a message (`messageId`)
* **reply**: Reply to a specific message (`messageId`, `text`, `to`)
* **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`)
* **renameGroup**: Rename a group chat (`chatGuid`, `displayName`)
* **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) β flaky on macOS 26 Tahoe (API may return success but the icon does not sync).
* **addParticipant**: Add someone to a group (`chatGuid`, `address`)
* **removeParticipant**: Remove someone from a group (`chatGuid`, `address`)
* **leaveGroup**: Leave a group chat (`chatGuid`)
* **sendAttachment**: Send media/files (`to`, `buffer`, `filename`, `asVoice`)
* Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 β CAF when sending voice memos.
Message IDs (short vs full) [#message-ids-short-vs-full]
Reeve may surface *short* message IDs (e.g., `1`, `2`) to save tokens.
* `MessageSid` / `ReplyToId` can be short IDs.
* `MessageSidFull` / `ReplyToIdFull` contain the provider full IDs.
* Short IDs are in-memory; they can expire on restart or cache eviction.
* Actions accept short or full `messageId`, but short IDs will error if no longer available.
Use full IDs for durable automations and storage:
* Templates: `{{MessageSidFull}}`, `{{ReplyToIdFull}}`
* Context: `MessageSidFull` / `ReplyToIdFull` in inbound payloads
See [Configuration](/docs/gateway/configuration) for template variables.
Block streaming [#block-streaming]
Control whether responses are sent as a single message or streamed in blocks:
```json5
{
channels: {
bluebubbles: {
blockStreaming: true // enable block streaming (default behavior)
}
}
}
```
Media + limits [#media--limits]
* Inbound attachments are downloaded and stored in the media cache.
* Media cap via `channels.bluebubbles.mediaMaxMb` (default: 8 MB).
* Outbound text is chunked to `channels.bluebubbles.textChunkLimit` (default: 4000 chars).
Configuration reference [#configuration-reference]
Full configuration: [Configuration](/docs/gateway/configuration)
Provider options:
* `channels.bluebubbles.enabled`: Enable/disable the channel.
* `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL.
* `channels.bluebubbles.password`: API password.
* `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`).
* `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`).
* `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`).
* `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`).
* `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
* `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
* `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
* `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `true`).
* `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
* `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
* `channels.bluebubbles.mediaMaxMb`: Inbound media cap in MB (default: 8).
* `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
* `channels.bluebubbles.dmHistoryLimit`: DM history limit.
* `channels.bluebubbles.actions`: Enable/disable specific actions.
* `channels.bluebubbles.accounts`: Multi-account configuration.
Related global options:
* `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).
* `messages.responsePrefix`.
Addressing / delivery targets [#addressing--delivery-targets]
Prefer `chat_guid` for stable routing:
* `chat_guid:iMessage;-;+15555550123` (preferred for groups)
* `chat_id:123`
* `chat_identifier:...`
* Direct handles: `+15555550123`, `user@example.com`
* If a direct handle does not have an existing DM chat, Reeve will create one via `POST /api/v1/chat/new`. This requires the BlueBubbles Private API to be enabled.
Security [#security]
* Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted.
* Keep the API password and webhook endpoint secret (treat them like credentials).
* Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN.
Troubleshooting [#troubleshooting]
* If typing/read events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`.
* Pairing codes expire after one hour; use `reeve pairing list bluebubbles` and `reeve pairing approve bluebubbles `.
* Reactions require the BlueBubbles private API (`POST /api/v1/message/react`); ensure the server version exposes it.
* Edit/unsend require macOS 13+ and a compatible BlueBubbles server version. On macOS 26 (Tahoe), edit is currently broken due to private API changes.
* Group icon updates can be flaky on macOS 26 (Tahoe): the API may return success but the new icon does not sync.
* Reeve auto-hides known-broken actions based on the BlueBubbles server's macOS version. If edit still appears on macOS 26 (Tahoe), disable it manually with `channels.bluebubbles.actions.edit=false`.
* For status/health info: `reeve status --all` or `reeve status --deep`.
For general channel workflow reference, see [Channels](/docs/channels) and the [Plugins](/docs/plugins) guide.
# Broadcast Groups (/docs/channels/broadcast-groups)
Broadcast Groups [#broadcast-groups]
**Status:** Experimental\
**Version:** Added in 2026.1.9
Overview [#overview]
Broadcast Groups enable multiple agents to process and respond to the same message simultaneously. This allows you to create specialized agent teams that work together in a single WhatsApp group or DM β all using one phone number.
Current scope: **WhatsApp only** (web channel).
Broadcast groups are evaluated after channel allowlists and group activation rules. In WhatsApp groups, this means broadcasts happen when Reeve would normally reply (for example: on mention, depending on your group settings).
Use Cases [#use-cases]
1. Specialized Agent Teams [#1-specialized-agent-teams]
Deploy multiple agents with atomic, focused responsibilities:
```
Group: "Development Team"
Agents:
- CodeReviewer (reviews code snippets)
- DocumentationBot (generates docs)
- SecurityAuditor (checks for vulnerabilities)
- TestGenerator (suggests test cases)
```
Each agent processes the same message and provides its specialized perspective.
2. Multi-Language Support [#2-multi-language-support]
```
Group: "International Support"
Agents:
- Agent_EN (responds in English)
- Agent_DE (responds in German)
- Agent_ES (responds in Spanish)
```
3. Quality Assurance Workflows [#3-quality-assurance-workflows]
```
Group: "Customer Support"
Agents:
- SupportAgent (provides answer)
- QAAgent (reviews quality, only responds if issues found)
```
4. Task Automation [#4-task-automation]
```
Group: "Project Management"
Agents:
- TaskTracker (updates task database)
- TimeLogger (logs time spent)
- ReportGenerator (creates summaries)
```
Configuration [#configuration]
Basic Setup [#basic-setup]
Add a top-level `broadcast` section (next to `bindings`). Keys are WhatsApp peer ids:
* group chats: group JID (e.g. `120363403215116621@g.us`)
* DMs: E.164 phone number (e.g. `+15551234567`)
```json
{
"broadcast": {
"120363403215116621@g.us": ["alfred", "baerbel", "assistant3"]
}
}
```
**Result:** When Reeve would reply in this chat, it will run all three agents.
Processing Strategy [#processing-strategy]
Control how agents process messages:
Parallel (Default) [#parallel-default]
All agents process simultaneously:
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
Sequential [#sequential]
Agents process in order (one waits for previous to finish):
```json
{
"broadcast": {
"strategy": "sequential",
"120363403215116621@g.us": ["alfred", "baerbel"]
}
}
```
Complete Example [#complete-example]
```json
{
"agents": {
"list": [
{
"id": "code-reviewer",
"name": "Code Reviewer",
"workspace": "/path/to/code-reviewer",
"sandbox": { "mode": "all" }
},
{
"id": "security-auditor",
"name": "Security Auditor",
"workspace": "/path/to/security-auditor",
"sandbox": { "mode": "all" }
},
{
"id": "docs-generator",
"name": "Documentation Generator",
"workspace": "/path/to/docs-generator",
"sandbox": { "mode": "all" }
}
]
},
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": ["code-reviewer", "security-auditor", "docs-generator"],
"120363424282127706@g.us": ["support-en", "support-de"],
"+15555550123": ["assistant", "logger"]
}
}
```
How It Works [#how-it-works]
Message Flow [#message-flow]
1. **Incoming message** arrives in a WhatsApp group
2. **Broadcast check**: System checks if peer ID is in `broadcast`
3. **If in broadcast list**:
* All listed agents process the message
* Each agent has its own session key and isolated context
* Agents process in parallel (default) or sequentially
4. **If not in broadcast list**:
* Normal routing applies (first matching binding)
Note: broadcast groups do not bypass channel allowlists or group activation rules (mentions/commands/etc). They only change *which agents run* when a message is eligible for processing.
Session Isolation [#session-isolation]
Each agent in a broadcast group maintains completely separate:
* **Session keys** (`agent:alfred:whatsapp:group:120363...` vs `agent:baerbel:whatsapp:group:120363...`)
* **Conversation history** (agent doesn't see other agents' messages)
* **Workspace** (separate sandboxes if configured)
* **Tool access** (different allow/deny lists)
* **Memory/context** (separate IDENTITY.md, SOUL.md, etc.)
* **Group context buffer** (recent group messages used for context) is shared per peer, so all broadcast agents see the same context when triggered
This allows each agent to have:
* Different personalities
* Different tool access (e.g., read-only vs. read-write)
* Different models (e.g., opus vs. sonnet)
* Different skills installed
Example: Isolated Sessions [#example-isolated-sessions]
In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`:
**Alfred's context:**
```
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses]
Workspace: /Users/pascal/reeve-alfred/
Tools: read, write, exec
```
**BΓ€rbel's context:**
```
Session: agent:baerbel:whatsapp:group:120363403215116621@g.us
History: [user message, baerbel's previous responses]
Workspace: /Users/pascal/reeve-baerbel/
Tools: read only
```
Best Practices [#best-practices]
1. Keep Agents Focused [#1-keep-agents-focused]
Design each agent with a single, clear responsibility:
```json
{
"broadcast": {
"DEV_GROUP": ["formatter", "linter", "tester"]
}
}
```
β **Good:** Each agent has one job\
β **Bad:** One generic "dev-helper" agent
2. Use Descriptive Names [#2-use-descriptive-names]
Make it clear what each agent does:
```json
{
"agents": {
"security-scanner": { "name": "Security Scanner" },
"code-formatter": { "name": "Code Formatter" },
"test-generator": { "name": "Test Generator" }
}
}
```
3. Configure Different Tool Access [#3-configure-different-tool-access]
Give agents only the tools they need:
```json
{
"agents": {
"reviewer": {
"tools": { "allow": ["read", "exec"] } // Read-only
},
"fixer": {
"tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
}
}
}
```
4. Monitor Performance [#4-monitor-performance]
With many agents, consider:
* Using `"strategy": "parallel"` (default) for speed
* Limiting broadcast groups to 5-10 agents
* Using faster models for simpler agents
5. Handle Failures Gracefully [#5-handle-failures-gracefully]
Agents fail independently. One agent's error doesn't block others:
```
Message β [Agent A β, Agent B β error, Agent C β]
Result: Agent A and C respond, Agent B logs error
```
Compatibility [#compatibility]
Providers [#providers]
Broadcast groups currently work with:
* β WhatsApp (implemented)
* π§ Telegram (planned)
* π§ Discord (planned)
* π§ Slack (planned)
Routing [#routing]
Broadcast groups work alongside existing routing:
```json
{
"bindings": [
{ "match": { "channel": "whatsapp", "peer": { "kind": "group", "id": "GROUP_A" } }, "agentId": "alfred" }
],
"broadcast": {
"GROUP_B": ["agent1", "agent2"]
}
}
```
* `GROUP_A`: Only alfred responds (normal routing)
* `GROUP_B`: agent1 AND agent2 respond (broadcast)
**Precedence:** `broadcast` takes priority over `bindings`.
Troubleshooting [#troubleshooting]
Agents Not Responding [#agents-not-responding]
**Check:**
1. Agent IDs exist in `agents.list`
2. Peer ID format is correct (e.g., `120363403215116621@g.us`)
3. Agents are not in deny lists
**Debug:**
```bash
tail -f ~/.reeve/logs/gateway.log | grep broadcast
```
Only One Agent Responding [#only-one-agent-responding]
**Cause:** Peer ID might be in `bindings` but not `broadcast`.
**Fix:** Add to broadcast config or remove from bindings.
Performance Issues [#performance-issues]
**If slow with many agents:**
* Reduce number of agents per group
* Use lighter models (sonnet instead of opus)
* Check sandbox startup time
Examples [#examples]
Example 1: Code Review Team [#example-1-code-review-team]
```json
{
"broadcast": {
"strategy": "parallel",
"120363403215116621@g.us": [
"code-formatter",
"security-scanner",
"test-coverage",
"docs-checker"
]
},
"agents": {
"list": [
{ "id": "code-formatter", "workspace": "~/agents/formatter", "tools": { "allow": ["read", "write"] } },
{ "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "exec"] } },
{ "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "exec"] } },
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
]
}
}
```
**User sends:** Code snippet\
**Responses:**
* code-formatter: "Fixed indentation and added type hints"
* security-scanner: "β οΈ SQL injection vulnerability in line 12"
* test-coverage: "Coverage is 45%, missing tests for error cases"
* docs-checker: "Missing docstring for function `process_data`"
Example 2: Multi-Language Support [#example-2-multi-language-support]
```json
{
"broadcast": {
"strategy": "sequential",
"+15555550123": ["detect-language", "translator-en", "translator-de"]
},
"agents": {
"list": [
{ "id": "detect-language", "workspace": "~/agents/lang-detect" },
{ "id": "translator-en", "workspace": "~/agents/translate-en" },
{ "id": "translator-de", "workspace": "~/agents/translate-de" }
]
}
}
```
API Reference [#api-reference]
Config Schema [#config-schema]
```typescript
interface ReeveConfig {
broadcast?: {
strategy?: "parallel" | "sequential";
[peerId: string]: string[];
};
}
```
Fields [#fields]
* `strategy` (optional): How to process agents
* `"parallel"` (default): All agents process simultaneously
* `"sequential"`: Agents process in array order
* `[peerId]`: WhatsApp group JID, E.164 number, or other peer ID
* Value: Array of agent IDs that should process messages
Limitations [#limitations]
1. **Max agents:** No hard limit, but 10+ agents may be slow
2. **Shared context:** Agents don't see each other's responses (by design)
3. **Message ordering:** Parallel responses may arrive in any order
4. **Rate limits:** All agents count toward WhatsApp rate limits
Future Enhancements [#future-enhancements]
Planned features:
* [ ] Shared context mode (agents see each other's responses)
* [ ] Agent coordination (agents can signal each other)
* [ ] Dynamic agent selection (choose agents based on message content)
* [ ] Agent priorities (some agents respond before others)
See Also [#see-also]
* [Multi-Agent Configuration](/multi-agent-sandbox-tools)
* [Routing Configuration](/docs/concepts/channel-routing)
* [Session Management](/docs/concepts/session)
# Discord (Bot API) (/docs/channels/discord)
Discord (Bot API) [#discord-bot-api]
Status: ready for DM and guild text channels via the official Discord bot gateway.
Quick setup (beginner) [#quick-setup-beginner]
1. Create a Discord bot and copy the bot token.
2. Set the token for Reeve:
* Env: `DISCORD_BOT_TOKEN=...`
* Or config: `channels.discord.token: "..."`.
* If both are set, config takes precedence (env fallback is default-account only).
3. Invite the bot to your server with message permissions.
4. Start the gateway.
5. DM access is pairing by default; approve the pairing code on first contact.
Minimal config:
```json5
{
channels: {
discord: {
enabled: true,
token: "YOUR_BOT_TOKEN"
}
}
}
```
Goals [#goals]
* Talk to Reeve via Discord DMs or guild channels.
* Direct chats collapse into the agent's main session (default `agent:main:main`); guild channels stay isolated as `agent::discord:channel:` (display names use `discord:#`).
* Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`.
* Keep routing deterministic: replies always go back to the channel they arrived on.
How it works [#how-it-works]
1. Create a Discord application β Bot, enable the intents you need (DMs + guild messages + message content), and grab the bot token.
2. Invite the bot to your server with the permissions required to read/send messages where you want to use it.
3. Configure Reeve with `channels.discord.token` (or `DISCORD_BOT_TOKEN` as a fallback).
4. Run the gateway; it auto-starts the Discord channel when a token is available (config first, env fallback) and `channels.discord.enabled` is not `false`.
* If you prefer env vars, set `DISCORD_BOT_TOKEN` (a config block is optional).
5. Direct chats: use `user:` (or a `<@id>` mention) when delivering; all turns land in the shared `main` session. Bare numeric IDs are ambiguous and rejected.
6. Guild channels: use `channel:` for delivery. Mentions are required by default and can be set per guild or per channel.
7. Direct chats: secure by default via `channels.discord.dm.policy` (default: `"pairing"`). Unknown senders get a pairing code (expires after 1 hour); approve via `reeve pairing approve discord `.
* To keep old βopen to anyoneβ behavior: set `channels.discord.dm.policy="open"` and `channels.discord.dm.allowFrom=["*"]`.
* To hard-allowlist: set `channels.discord.dm.policy="allowlist"` and list senders in `channels.discord.dm.allowFrom`.
* To ignore all DMs: set `channels.discord.dm.enabled=false` or `channels.discord.dm.policy="disabled"`.
8. Group DMs are ignored by default; enable via `channels.discord.dm.groupEnabled` and optionally restrict by `channels.discord.dm.groupChannels`.
9. Optional guild rules: set `channels.discord.guilds` keyed by guild id (preferred) or slug, with per-channel rules.
10. Optional native commands: `commands.native` defaults to `"auto"` (on for Discord/Telegram, off for Slack). Override with `channels.discord.commands.native: true|false|"auto"`; `false` clears previously registered commands. Text commands are controlled by `commands.text` and must be sent as standalone `/...` messages. Use `commands.useAccessGroups: false` to bypass access-group checks for commands.
* Full command list + config: [Slash commands](/docs/tools/slash-commands)
11. Optional guild context history: set `channels.discord.historyLimit` (default 20, falls back to `messages.groupChat.historyLimit`) to include the last N guild messages as context when replying to a mention. Set `0` to disable.
12. Reactions: the agent can trigger reactions via the `discord` tool (gated by `channels.discord.actions.*`).
* Reaction removal semantics: see [/tools/reactions](/docs/tools/reactions).
* The `discord` tool is only exposed when the current channel is Discord.
13. Native commands use isolated session keys (`agent::discord:slash:`) rather than the shared `main` session.
Note: Name β id resolution uses guild member search and requires Server Members Intent; if the bot canβt search members, use ids or `<@id>` mentions.
Note: Slugs are lowercase with spaces replaced by `-`. Channel names are slugged without the leading `#`.
Note: Guild context `[from:]` lines include `author.tag` + `id` to make ping-ready replies easy.
Config writes [#config-writes]
By default, Discord is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
Disable with:
```json5
{
channels: { discord: { configWrites: false } }
}
```
How to create your own bot [#how-to-create-your-own-bot]
This is the βDiscord Developer Portalβ setup for running Reeve in a server (guild) channel like `#help`.
1) Create the Discord app + bot user [#1-create-the-discord-app--bot-user]
1. Discord Developer Portal β **Applications** β **New Application**
2. In your app:
* **Bot** β **Add Bot**
* Copy the **Bot Token** (this is what you put in `DISCORD_BOT_TOKEN`)
2) Enable the gateway intents Reeve needs [#2-enable-the-gateway-intents-reeve-needs]
Discord blocks βprivileged intentsβ unless you explicitly enable them.
In **Bot** β **Privileged Gateway Intents**, enable:
* **Message Content Intent** (required to read message text in most guilds; without it youβll see βUsed disallowed intentsβ or the bot will connect but not react to messages)
* **Server Members Intent** (recommended; required for some member/user lookups and allowlist matching in guilds)
You usually do **not** need **Presence Intent**.
3) Generate an invite URL (OAuth2 URL Generator) [#3-generate-an-invite-url-oauth2-url-generator]
In your app: **OAuth2** β **URL Generator**
**Scopes**
* β `bot`
* β `applications.commands` (required for native commands)
**Bot Permissions** (minimal baseline)
* β View Channels
* β Send Messages
* β Read Message History
* β Embed Links
* β Attach Files
* β Add Reactions (optional but recommended)
* β Use External Emojis / Stickers (optional; only if you want them)
Avoid **Administrator** unless youβre debugging and fully trust the bot.
Copy the generated URL, open it, pick your server, and install the bot.
4) Get the ids (guild/user/channel) [#4-get-the-ids-guilduserchannel]
Discord uses numeric ids everywhere; Reeve config prefers ids.
1. Discord (desktop/web) β **User Settings** β **Advanced** β enable **Developer Mode**
2. Right-click:
* Server name β **Copy Server ID** (guild id)
* Channel (e.g. `#help`) β **Copy Channel ID**
* Your user β **Copy User ID**
5) Configure Reeve [#5-configure-reeve]
Token [#token]
Set the bot token via env var (recommended on servers):
* `DISCORD_BOT_TOKEN=...`
Or via config:
```json5
{
channels: {
discord: {
enabled: true,
token: "YOUR_BOT_TOKEN"
}
}
}
```
Multi-account support: use `channels.discord.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/docs/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
Allowlist + channel routing [#allowlist--channel-routing]
Example βsingle server, only allow me, only allow #helpβ:
```json5
{
channels: {
discord: {
enabled: true,
dm: { enabled: false },
guilds: {
"YOUR_GUILD_ID": {
users: ["YOUR_USER_ID"],
requireMention: true,
channels: {
help: { allow: true, requireMention: true }
}
}
},
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1
}
}
}
}
```
Notes:
* `requireMention: true` means the bot only replies when mentioned (recommended for shared channels).
* `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions for guild messages.
* Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
* If `channels` is present, any channel not listed is denied by default.
* Use a `"*"` channel entry to apply defaults across all channels; explicit channel entries override the wildcard.
* Threads inherit parent channel config (allowlist, `requireMention`, skills, prompts, etc.) unless you add the thread channel id explicitly.
* Bot-authored messages are ignored by default; set `channels.discord.allowBots=true` to allow them (own messages remain filtered).
* Warning: If you allow replies to other bots (`channels.discord.allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.discord.guilds.*.channels..users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
6) Verify it works [#6-verify-it-works]
1. Start the gateway.
2. In your server channel, send: `@Krill hello` (or whatever your bot name is).
3. If nothing happens: check **Troubleshooting** below.
Troubleshooting [#troubleshooting]
* First: run `reeve doctor` and `reeve channels status --probe` (actionable warnings + quick audits).
* **βUsed disallowed intentsβ**: enable **Message Content Intent** (and likely **Server Members Intent**) in the Developer Portal, then restart the gateway.
* **Bot connects but never replies in a guild channel**:
* Missing **Message Content Intent**, or
* The bot lacks channel permissions (View/Send/Read History), or
* Your config requires mentions and you didnβt mention it, or
* Your guild/channel allowlist denies the channel/user.
* **`requireMention: false` but still no replies**:
* `channels.discord.groupPolicy` defaults to **allowlist**; set it to `"open"` or add a guild entry under `channels.discord.guilds` (optionally list channels under `channels.discord.guilds..channels` to restrict).
* If you only set `DISCORD_BOT_TOKEN` and never create a `channels.discord` section, the runtime
defaults `groupPolicy` to `open`. Add `channels.discord.groupPolicy`,
`channels.defaults.groupPolicy`, or a guild/channel allowlist to lock it down.
* `requireMention` must live under `channels.discord.guilds` (or a specific channel). `channels.discord.requireMention` at the top level is ignored.
* **Permission audits** (`channels status --probe`) only check numeric channel IDs. If you use slugs/names as `channels.discord.guilds.*.channels` keys, the audit canβt verify permissions.
* **DMs donβt work**: `channels.discord.dm.enabled=false`, `channels.discord.dm.policy="disabled"`, or you havenβt been approved yet (`channels.discord.dm.policy="pairing"`).
Capabilities & limits [#capabilities--limits]
* DMs and guild text channels (threads are treated as separate channels; voice not supported).
* Typing indicators sent best-effort; message chunking uses `channels.discord.textChunkLimit` (default 2000) and splits tall replies by line count (`channels.discord.maxLinesPerMessage`, default 17).
* Optional newline chunking: set `channels.discord.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
* File uploads supported up to the configured `channels.discord.mediaMaxMb` (default 8 MB).
* Mention-gated guild replies by default to avoid noisy bots.
* Reply context is injected when a message references another message (quoted content + ids).
* Native reply threading is **off by default**; enable with `channels.discord.replyToMode` and reply tags.
Retry policy [#retry-policy]
Outbound Discord API calls retry on rate limits (429) using Discord `retry_after` when available, with exponential backoff and jitter. Configure via `channels.discord.retry`. See [Retry policy](/docs/concepts/retry).
Config [#config]
```json5
{
channels: {
discord: {
enabled: true,
token: "abc.123",
groupPolicy: "allowlist",
guilds: {
"*": {
channels: {
general: { allow: true }
}
}
},
mediaMaxMb: 8,
actions: {
reactions: true,
stickers: true,
emojiUploads: true,
stickerUploads: true,
polls: true,
permissions: true,
messages: true,
threads: true,
pins: true,
search: true,
memberInfo: true,
roleInfo: true,
roles: false,
channelInfo: true,
channels: true,
voiceStatus: true,
events: true,
moderation: false
},
replyToMode: "off",
dm: {
enabled: true,
policy: "pairing", // pairing | allowlist | open | disabled
allowFrom: ["123456789012345678", "steipete"],
groupEnabled: false,
groupChannels: ["reeve-dm"]
},
guilds: {
"*": { requireMention: true },
"123456789012345678": {
slug: "friends-of-reeve",
requireMention: false,
reactionNotifications: "own",
users: ["987654321098765432", "steipete"],
channels: {
general: { allow: true },
help: {
allow: true,
requireMention: true,
users: ["987654321098765432"],
skills: ["search", "docs"],
systemPrompt: "Keep answers short."
}
}
}
}
}
}
}
```
Ack reactions are controlled globally via `messages.ackReaction` +
`messages.ackReactionScope`. Use `messages.removeAckAfterReply` to clear the
ack reaction after the bot replies.
* `dm.enabled`: set `false` to ignore all DMs (default `true`).
* `dm.policy`: DM access control (`pairing` recommended). `"open"` requires `dm.allowFrom=["*"]`.
* `dm.allowFrom`: DM allowlist (user ids or names). Used by `dm.policy="allowlist"` and for `dm.policy="open"` validation. The wizard accepts usernames and resolves them to ids when the bot can search members.
* `dm.groupEnabled`: enable group DMs (default `false`).
* `dm.groupChannels`: optional allowlist for group DM channel ids or slugs.
* `groupPolicy`: controls guild channel handling (`open|disabled|allowlist`); `allowlist` requires channel allowlists.
* `guilds`: per-guild rules keyed by guild id (preferred) or slug.
* `guilds."*"`: default per-guild settings applied when no explicit entry exists.
* `guilds..slug`: optional friendly slug used for display names.
* `guilds..users`: optional per-guild user allowlist (ids or names).
* `guilds..channels..allow`: allow/deny the channel when `groupPolicy="allowlist"`.
* `guilds..channels..requireMention`: mention gating for the channel.
* `guilds..channels..users`: optional per-channel user allowlist.
* `guilds..channels..skills`: skill filter (omit = all skills, empty = none).
* `guilds..channels..systemPrompt`: extra system prompt for the channel (combined with channel topic).
* `guilds..channels..enabled`: set `false` to disable the channel.
* `guilds..channels`: channel rules (keys are channel slugs or ids).
* `guilds..requireMention`: per-guild mention requirement (overridable per channel).
* `guilds..reactionNotifications`: reaction system event mode (`off`, `own`, `all`, `allowlist`).
* `textChunkLimit`: outbound text chunk size (chars). Default: 2000.
* `chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
* `maxLinesPerMessage`: soft max line count per message. Default: 17.
* `mediaMaxMb`: clamp inbound media saved to disk.
* `historyLimit`: number of recent guild messages to include as context when replying to a mention (default 20; falls back to `messages.groupChat.historyLimit`; `0` disables).
* `dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `dms[""].historyLimit`.
* `retry`: retry policy for outbound Discord API calls (attempts, minDelayMs, maxDelayMs, jitter).
* `actions`: per-action tool gates; omit to allow all (set `false` to disable).
* `reactions` (covers react + read reactions)
* `stickers`, `emojiUploads`, `stickerUploads`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search`
* `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events`
* `channels` (create/edit/delete channels + categories + permissions)
* `roles` (role add/remove, default `false`)
* `moderation` (timeout/kick/ban, default `false`)
Reaction notifications use `guilds..reactionNotifications`:
* `off`: no reaction events.
* `own`: reactions on the bot's own messages (default).
* `all`: all reactions on all messages.
* `allowlist`: reactions from `guilds..users` on all messages (empty list disables).
Tool action defaults [#tool-action-defaults]
| Action group | Default | Notes |
| -------------- | -------- | ---------------------------------- |
| reactions | enabled | React + list reactions + emojiList |
| stickers | enabled | Send stickers |
| emojiUploads | enabled | Upload emojis |
| stickerUploads | enabled | Upload stickers |
| polls | enabled | Create polls |
| permissions | enabled | Channel permission snapshot |
| messages | enabled | Read/send/edit/delete |
| threads | enabled | Create/list/reply |
| pins | enabled | Pin/unpin/list |
| search | enabled | Message search (preview feature) |
| memberInfo | enabled | Member info |
| roleInfo | enabled | Role list |
| channelInfo | enabled | Channel info + list |
| channels | enabled | Channel/category management |
| voiceStatus | enabled | Voice state lookup |
| events | enabled | List/create scheduled events |
| roles | disabled | Role add/remove |
| moderation | disabled | Timeout/kick/ban |
* `replyToMode`: `off` (default), `first`, or `all`. Applies only when the model includes a reply tag.
Reply tags [#reply-tags]
To request a threaded reply, the model can include one tag in its output:
* `[[reply_to_current]]` β reply to the triggering Discord message.
* `[[reply_to:]]` β reply to a specific message id from context/history.
Current message ids are appended to prompts as `[message_id: β¦]`; history entries already include ids.
Behavior is controlled by `channels.discord.replyToMode`:
* `off`: ignore tags.
* `first`: only the first outbound chunk/attachment is a reply.
* `all`: every outbound chunk/attachment is a reply.
Allowlist matching notes:
* `allowFrom`/`users`/`groupChannels` accept ids, names, tags, or mentions like `<@id>`.
* Prefixes like `discord:`/`user:` (users) and `channel:` (group DMs) are supported.
* Use `*` to allow any sender/channel.
* When `guilds..channels` is present, channels not listed are denied by default.
* When `guilds..channels` is omitted, all channels in the allowlisted guild are allowed.
* To allow **no channels**, set `channels.discord.groupPolicy: "disabled"` (or keep an empty allowlist).
* The configure wizard accepts `Guild/Channel` names (public + private) and resolves them to IDs when possible.
* On startup, Reeve resolves channel/user names in allowlists to IDs (when the bot can search members)
and logs the mapping; unresolved entries are kept as typed.
Native command notes:
* The registered commands mirror Reeveβs chat commands.
* Native commands honor the same allowlists as DMs/guild messages (`channels.discord.dm.allowFrom`, `channels.discord.guilds`, per-channel rules).
* Slash commands may still be visible in Discord UI to users who arenβt allowlisted; Reeve enforces allowlists on execution and replies βnot authorizedβ.
Tool actions [#tool-actions]
The agent can call `discord` with actions like:
* `react` / `reactions` (add or list reactions)
* `sticker`, `poll`, `permissions`
* `readMessages`, `sendMessage`, `editMessage`, `deleteMessage`
* Read/search/pin tool payloads include normalized `timestampMs` (UTC epoch ms) and `timestampUtc` alongside raw Discord `timestamp`.
* `threadCreate`, `threadList`, `threadReply`
* `pinMessage`, `unpinMessage`, `listPins`
* `searchMessages`, `memberInfo`, `roleInfo`, `roleAdd`, `roleRemove`, `emojiList`
* `channelInfo`, `channelList`, `voiceStatus`, `eventList`, `eventCreate`
* `timeout`, `kick`, `ban`
Discord message ids are surfaced in the injected context (`[discord message id: β¦]` and history lines) so the agent can target them.
Emoji can be unicode (e.g., `β `) or custom emoji syntax like `<:party_blob:1234567890>`.
Safety & ops [#safety--ops]
* Treat the bot token like a password; prefer the `DISCORD_BOT_TOKEN` env var on supervised hosts or lock down the config file permissions.
* Only grant the bot permissions it needs (typically Read/Send Messages).
* If the bot is stuck or rate limited, restart the gateway (`reeve gateway --force`) after confirming no other processes own the Discord session.
# Google Chat (Chat API) (/docs/channels/googlechat)
Google Chat (Chat API) [#google-chat-chat-api]
Status: ready for DMs + spaces via Google Chat API webhooks (HTTP only).
Quick setup (beginner) [#quick-setup-beginner]
1. Create a Google Cloud project and enable the **Google Chat API**.
* Go to: [Google Chat API Credentials](https://console.cloud.google.com/apis/api/chat.googleapis.com/credentials)
* Enable the API if it is not already enabled.
2. Create a **Service Account**:
* Press **Create Credentials** > **Service Account**.
* Name it whatever you want (e.g., `reeve-chat`).
* Leave permissions blank (press **Continue**).
* Leave principals with access blank (press **Done**).
3. Create and download the **JSON Key**:
* In the list of service accounts, click on the one you just created.
* Go to the **Keys** tab.
* Click **Add Key** > **Create new key**.
* Select **JSON** and press **Create**.
4. Store the downloaded JSON file on your gateway host (e.g., `~/.reeve/googlechat-service-account.json`).
5. Create a Google Chat app in the [Google Cloud Console Chat Configuration](https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat):
* Fill in the **Application info**:
* **App name**: (e.g. `Reeve`)
* **Avatar URL**: (e.g. `https://reeve.com/logo.png`)
* **Description**: (e.g. `Personal AI Assistant`)
* Enable **Interactive features**.
* Under **Functionality**, check **Join spaces and group conversations**.
* Under **Connection settings**, select **HTTP endpoint URL**.
* Under **Triggers**, select **Use a common HTTP endpoint URL for all triggers** and set it to your gateway's public URL followed by `/googlechat`.
* *Tip: Run `reeve status` to find your gateway's public URL.*
* Under **Visibility**, check **Make this Chat app available to specific people and groups in \**.
* Enter your email address (e.g. `user@example.com`) in the text box.
* Click **Save** at the bottom.
6. **Enable the app status**:
* After saving, **refresh the page**.
* Look for the **App status** section (usually near the top or bottom after saving).
* Change the status to **Live - available to users**.
* Click **Save** again.
7. Configure Reeve with the service account path + webhook audience:
* Env: `GOOGLE_CHAT_SERVICE_ACCOUNT_FILE=/path/to/service-account.json`
* Or config: `channels.googlechat.serviceAccountFile: "/path/to/service-account.json"`.
8. Set the webhook audience type + value (matches your Chat app config).
9. Start the gateway. Google Chat will POST to your webhook path.
Add to Google Chat [#add-to-google-chat]
Once the gateway is running and your email is added to the visibility list:
1. Go to [Google Chat](https://chat.google.com/).
2. Click the **+** (plus) icon next to **Direct Messages**.
3. In the search bar (where you usually add people), type the **App name** you configured in the Google Cloud Console.
* **Note**: The bot will *not* appear in the "Marketplace" browse list because it is a private app. You must search for it by name.
4. Select your bot from the results.
5. Click **Add** or **Chat** to start a 1:1 conversation.
6. Send "Hello" to trigger the assistant!
Public URL (Webhook-only) [#public-url-webhook-only]
Google Chat webhooks require a public HTTPS endpoint. For security, **only expose the `/googlechat` path** to the internet. Keep the Reeve dashboard and other sensitive endpoints on your private network.
Option A: Tailscale Funnel (Recommended) [#option-a-tailscale-funnel-recommended]
Use Tailscale Serve for the private dashboard and Funnel for the public webhook path. This keeps `/` private while exposing only `/googlechat`.
1. **Check what address your gateway is bound to:**
```bash
ss -tlnp | grep 18789
```
Note the IP address (e.g., `127.0.0.1`, `0.0.0.0`, or your Tailscale IP like `100.x.x.x`).
2. **Expose the dashboard to the tailnet only (port 8443):**
```bash
# If bound to localhost (127.0.0.1 or 0.0.0.0):
tailscale serve --bg --https 8443 http://127.0.0.1:18789
# If bound to Tailscale IP only (e.g., 100.106.161.80):
tailscale serve --bg --https 8443 http://100.106.161.80:18789
```
3. **Expose only the webhook path publicly:**
```bash
# If bound to localhost (127.0.0.1 or 0.0.0.0):
tailscale funnel --bg --set-path /googlechat http://127.0.0.1:18789/googlechat
# If bound to Tailscale IP only (e.g., 100.106.161.80):
tailscale funnel --bg --set-path /googlechat http://100.106.161.80:18789/googlechat
```
4. **Authorize the node for Funnel access:**
If prompted, visit the authorization URL shown in the output to enable Funnel for this node in your tailnet policy.
5. **Verify the configuration:**
```bash
tailscale serve status
tailscale funnel status
```
Your public webhook URL will be:
`https://..ts.net/googlechat`
Your private dashboard stays tailnet-only:
`https://..ts.net:8443/`
Use the public URL (without `:8443`) in the Google Chat app config.
> Note: This configuration persists across reboots. To remove it later, run `tailscale funnel reset` and `tailscale serve reset`.
Option B: Reverse Proxy (Caddy) [#option-b-reverse-proxy-caddy]
If you use a reverse proxy like Caddy, only proxy the specific path:
```nginx
your-domain.com {
reverse_proxy /googlechat* localhost:18789
}
```
With this config, any request to `your-domain.com/` will be ignored or returned as 404, while `your-domain.com/googlechat` is safely routed to Reeve.
Option C: Cloudflare Tunnel [#option-c-cloudflare-tunnel]
Configure your tunnel's ingress rules to only route the webhook path:
* **Path**: `/googlechat` -> `http://localhost:18789/googlechat`
* **Default Rule**: HTTP 404 (Not Found)
How it works [#how-it-works]
1. Google Chat sends webhook POSTs to the gateway. Each request includes an `Authorization: Bearer ` header.
2. Reeve verifies the token against the configured `audienceType` + `audience`:
* `audienceType: "app-url"` β audience is your HTTPS webhook URL.
* `audienceType: "project-number"` β audience is the Cloud project number.
3. Messages are routed by space:
* DMs use session key `agent::googlechat:dm:`.
* Spaces use session key `agent::googlechat:group:`.
4. DM access is pairing by default. Unknown senders receive a pairing code; approve with:
* `reeve pairing approve googlechat `
5. Group spaces require @-mention by default. Use `botUser` if mention detection needs the appβs user name.
Targets [#targets]
Use these identifiers for delivery and allowlists:
* Direct messages: `users/` or `users/` (email addresses are accepted).
* Spaces: `spaces/`.
Config highlights [#config-highlights]
```json5
{
channels: {
"googlechat": {
enabled: true,
serviceAccountFile: "/path/to/service-account.json",
audienceType: "app-url",
audience: "https://gateway.example.com/googlechat",
webhookPath: "/googlechat",
botUser: "users/1234567890", // optional; helps mention detection
dm: {
policy: "pairing",
allowFrom: ["users/1234567890", "name@example.com"]
},
groupPolicy: "allowlist",
groups: {
"spaces/AAAA": {
allow: true,
requireMention: true,
users: ["users/1234567890"],
systemPrompt: "Short answers only."
}
},
actions: { reactions: true },
typingIndicator: "message",
mediaMaxMb: 20
}
}
}
```
Notes:
* Service account credentials can also be passed inline with `serviceAccount` (JSON string).
* Default webhook path is `/googlechat` if `webhookPath` isnβt set.
* Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled.
* `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth).
* Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
Troubleshooting [#troubleshooting]
405 Method Not Allowed [#405-method-not-allowed]
If Google Cloud Logs Explorer shows errors like:
```
status code: 405, reason phrase: HTTP error response: HTTP/1.1 405 Method Not Allowed
```
This means the webhook handler isn't registered. Common causes:
1. **Channel not configured**: The `channels.googlechat` section is missing from your config. Verify with:
```bash
reeve config get channels.googlechat
```
If it returns "Config path not found", add the configuration (see [Config highlights](#config-highlights)).
2. **Plugin not enabled**: Check plugin status:
```bash
reeve plugins list | grep googlechat
```
If it shows "disabled", add `plugins.entries.googlechat.enabled: true` to your config.
3. **Gateway not restarted**: After adding config, restart the gateway:
```bash
reeve gateway restart
```
Verify the channel is running:
```bash
reeve channels status
# Should show: Google Chat default: enabled, configured, ...
```
Other issues [#other-issues]
* Check `reeve channels status --probe` for auth errors or missing audience config.
* If no messages arrive, confirm the Chat app's webhook URL + event subscriptions.
* If mention gating blocks replies, set `botUser` to the app's user resource name and verify `requireMention`.
* Use `reeve logs --follow` while sending a test message to see if requests reach the gateway.
Related docs:
* [Gateway configuration](/docs/gateway/configuration)
* [Security](/docs/gateway/security)
* [Reactions](/docs/tools/reactions)
# grammY Integration (Telegram Bot API) (/docs/channels/grammy)
grammY Integration (Telegram Bot API) [#grammy-integration-telegram-bot-api]
Why grammY [#why-grammy]
* TS-first Bot API client with built-in long-poll + webhook helpers, middleware, error handling, rate limiter.
* Cleaner media helpers than hand-rolling fetch + FormData; supports all Bot API methods.
* Extensible: proxy support via custom fetch, session middleware (optional), type-safe context.
What we shipped [#what-we-shipped]
* **Single client path:** fetch-based implementation removed; grammY is now the sole Telegram client (send + gateway) with the grammY throttler enabled by default.
* **Gateway:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`.
* **Proxy:** optional `channels.telegram.proxy` uses `undici.ProxyAgent` through grammYβs `client.baseFetch`.
* **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` is set (otherwise it long-polls).
* **Sessions:** direct chats collapse into the agent main session (`agent::`); groups use `agent::telegram:group:`; replies route back to the same channel.
* **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`.
* **Draft streaming:** optional `channels.telegram.streamMode` uses `sendMessageDraft` in private topic chats (Bot API 9.3+). This is separate from channel block streaming.
* **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
Open questions
* Optional grammY plugins (throttler) if we hit Bot API 429s.
* Add more structured media tests (stickers, voice notes).
* Make webhook listen port configurable (currently fixed to 8787 unless wired through the gateway).
# iMessage (imsg) (/docs/channels/imessage)
iMessage (imsg) [#imessage-imsg]
Status: external CLI integration. Gateway spawns `imsg rpc` (JSON-RPC over stdio).
Quick setup (beginner) [#quick-setup-beginner]
1. Ensure Messages is signed in on this Mac.
2. Install `imsg`:
* `brew install steipete/tap/imsg`
3. Configure Reeve with `channels.imessage.cliPath` and `channels.imessage.dbPath`.
4. Start the gateway and approve any macOS prompts (Automation + Full Disk Access).
Minimal config:
```json5
{
channels: {
imessage: {
enabled: true,
cliPath: "/usr/local/bin/imsg",
dbPath: "/Users//Library/Messages/chat.db"
}
}
}
```
What it is [#what-it-is]
* iMessage channel backed by `imsg` on macOS.
* Deterministic routing: replies always go back to iMessage.
* DMs share the agent's main session; groups are isolated (`agent::imessage:group:`).
* If a multi-participant thread arrives with `is_group=false`, you can still isolate it by `chat_id` using `channels.imessage.groups` (see βGroup-ish threadsβ below).
Config writes [#config-writes]
By default, iMessage is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
Disable with:
```json5
{
channels: { imessage: { configWrites: false } }
}
```
Requirements [#requirements]
* macOS with Messages signed in.
* Full Disk Access for Reeve + `imsg` (Messages DB access).
* Automation permission when sending.
* `channels.imessage.cliPath` can point to any command that proxies stdin/stdout (for example, a wrapper script that SSHes to another Mac and runs `imsg rpc`).
Setup (fast path) [#setup-fast-path]
1. Ensure Messages is signed in on this Mac.
2. Configure iMessage and start the gateway.
Dedicated bot macOS user (for isolated identity) [#dedicated-bot-macos-user-for-isolated-identity]
If you want the bot to send from a **separate iMessage identity** (and keep your personal Messages clean), use a dedicated Apple ID + a dedicated macOS user.
1. Create a dedicated Apple ID (example: `my-cool-bot@icloud.com`).
* Apple may require a phone number for verification / 2FA.
2. Create a macOS user (example: `reeveshome`) and sign into it.
3. Open Messages in that macOS user and sign into iMessage using the bot Apple ID.
4. Enable Remote Login (System Settings β General β Sharing β Remote Login).
5. Install `imsg`:
* `brew install steipete/tap/imsg`
6. Set up SSH so `ssh @localhost true` works without a password.
7. Point `channels.imessage.accounts.bot.cliPath` at an SSH wrapper that runs `imsg` as the bot user.
First-run note: sending/receiving may require GUI approvals (Automation + Full Disk Access) in the *bot macOS user*. If `imsg rpc` looks stuck or exits, log into that user (Screen Sharing helps), run a one-time `imsg chats --limit 1` / `imsg send ...`, approve prompts, then retry.
Example wrapper (`chmod +x`). Replace `` with your actual macOS username:
```bash
#!/usr/bin/env bash
set -euo pipefail
# Run an interactive SSH once first to accept host keys:
# ssh @localhost true
exec /usr/bin/ssh -o BatchMode=yes -o ConnectTimeout=5 -T @localhost \
"/usr/local/bin/imsg" "$@"
```
Example config:
```json5
{
channels: {
imessage: {
enabled: true,
accounts: {
bot: {
name: "Bot",
enabled: true,
cliPath: "/path/to/imsg-bot",
dbPath: "/Users//Library/Messages/chat.db"
}
}
}
}
}
```
For single-account setups, use flat options (`channels.imessage.cliPath`, `channels.imessage.dbPath`) instead of the `accounts` map.
Remote/SSH variant (optional) [#remotessh-variant-optional]
If you want iMessage on another Mac, set `channels.imessage.cliPath` to a wrapper that runs `imsg` on the remote macOS host over SSH. Reeve only needs stdio.
Example wrapper:
```bash
#!/usr/bin/env bash
exec ssh -T gateway-host imsg "$@"
```
**Remote attachments:** When `cliPath` points to a remote host via SSH, attachment paths in the Messages database reference files on the remote machine. Reeve can automatically fetch these over SCP by setting `channels.imessage.remoteHost`:
```json5
{
channels: {
imessage: {
cliPath: "~/imsg-ssh", // SSH wrapper to remote Mac
remoteHost: "user@gateway-host", // for SCP file transfer
includeAttachments: true
}
}
}
```
If `remoteHost` is not set, Reeve attempts to auto-detect it by parsing the SSH command in your wrapper script. Explicit configuration is recommended for reliability.
Remote Mac via Tailscale (example) [#remote-mac-via-tailscale-example]
If the Gateway runs on a Linux host/VM but iMessage must run on a Mac, Tailscale is the simplest bridge: the Gateway talks to the Mac over the tailnet, runs `imsg` via SSH, and SCPs attachments back.
Architecture:
```
ββββββββββββββββββββββββββββββββ SSH (imsg rpc) ββββββββββββββββββββββββββββ
β Gateway host (Linux/VM) ββββββββββββββββββββββββββββββββββββΆβ Mac with Messages + imsg β
β - reeve gateway β SCP (attachments) β - Messages signed in β
β - channels.imessage.cliPath βββββββββββββββββββββββββββββββββββββ - Remote Login enabled β
ββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
β²
β Tailscale tailnet (hostname or 100.x.y.z)
βΌ
user@gateway-host
```
Concrete config example (Tailscale hostname):
```json5
{
channels: {
imessage: {
enabled: true,
cliPath: "~/.reeve/scripts/imsg-ssh",
remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
includeAttachments: true,
dbPath: "/Users/bot/Library/Messages/chat.db"
}
}
}
```
Example wrapper (`~/.reeve/scripts/imsg-ssh`):
```bash
#!/usr/bin/env bash
exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
```
Notes:
* Ensure the Mac is signed in to Messages, and Remote Login is enabled.
* Use SSH keys so `ssh bot@mac-mini.tailnet-1234.ts.net` works without prompts.
* `remoteHost` should match the SSH target so SCP can fetch attachments.
Multi-account support: use `channels.imessage.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/docs/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. Don't commit `~/.reeve/reeve.json` (it often contains tokens).
Access control (DMs + groups) [#access-control-dms--groups]
DMs:
* Default: `channels.imessage.dmPolicy = "pairing"`.
* Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
* Approve via:
* `reeve pairing list imessage`
* `reeve pairing approve imessage `
* Pairing is the default token exchange for iMessage DMs. Details: [Pairing](/docs/start/pairing)
Groups:
* `channels.imessage.groupPolicy = open | allowlist | disabled`.
* `channels.imessage.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
* Mention gating uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) because iMessage has no native mention metadata.
* Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
How it works (behavior) [#how-it-works-behavior]
* `imsg` streams message events; the gateway normalizes them into the shared channel envelope.
* Replies always route back to the same chat id or handle.
Group-ish threads (is_group=false) [#group-ish-threads-is_groupfalse]
Some iMessage threads can have multiple participants but still arrive with `is_group=false` depending on how Messages stores the chat identifier.
If you explicitly configure a `chat_id` under `channels.imessage.groups`, Reeve treats that thread as a βgroupβ for:
* session isolation (separate `agent::imessage:group:` session key)
* group allowlisting / mention gating behavior
Example:
```json5
{
channels: {
imessage: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15555550123"],
groups: {
"42": { "requireMention": false }
}
}
}
}
```
This is useful when you want an isolated personality/model for a specific thread (see [Multi-agent routing](/docs/concepts/multi-agent)). For filesystem isolation, see [Sandboxing](/docs/gateway/sandboxing).
Media + limits [#media--limits]
* Optional attachment ingestion via `channels.imessage.includeAttachments`.
* Media cap via `channels.imessage.mediaMaxMb`.
Limits [#limits]
* Outbound text is chunked to `channels.imessage.textChunkLimit` (default 4000).
* Optional newline chunking: set `channels.imessage.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
* Media uploads are capped by `channels.imessage.mediaMaxMb` (default 16).
Addressing / delivery targets [#addressing--delivery-targets]
Prefer `chat_id` for stable routing:
* `chat_id:123` (preferred)
* `chat_guid:...`
* `chat_identifier:...`
* direct handles: `imessage:+1555` / `sms:+1555` / `user@example.com`
List chats:
```
imsg chats --limit 20
```
Configuration reference (iMessage) [#configuration-reference-imessage]
Full configuration: [Configuration](/docs/gateway/configuration)
Provider options:
* `channels.imessage.enabled`: enable/disable channel startup.
* `channels.imessage.cliPath`: path to `imsg`.
* `channels.imessage.dbPath`: Messages DB path.
* `channels.imessage.remoteHost`: SSH host for SCP attachment transfer when `cliPath` points to a remote Mac (e.g., `user@gateway-host`). Auto-detected from SSH wrapper if not set.
* `channels.imessage.service`: `imessage | sms | auto`.
* `channels.imessage.region`: SMS region.
* `channels.imessage.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
* `channels.imessage.allowFrom`: DM allowlist (handles, emails, E.164 numbers, or `chat_id:*`). `open` requires `"*"`. iMessage has no usernames; use handles or chat targets.
* `channels.imessage.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
* `channels.imessage.groupAllowFrom`: group sender allowlist.
* `channels.imessage.historyLimit` / `channels.imessage.accounts.*.historyLimit`: max group messages to include as context (0 disables).
* `channels.imessage.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.imessage.dms[""].historyLimit`.
* `channels.imessage.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
* `channels.imessage.includeAttachments`: ingest attachments into context.
* `channels.imessage.mediaMaxMb`: inbound/outbound media cap (MB).
* `channels.imessage.textChunkLimit`: outbound chunk size (chars).
* `channels.imessage.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
Related global options:
* `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).
* `messages.responsePrefix`.
# Chat Channels (/docs/channels)
Chat Channels [#chat-channels]
Reeve can talk to you on any chat app you already use. Each channel connects via the Gateway.
Text is supported everywhere; media and reactions vary by channel.
Supported channels [#supported-channels]
* [WhatsApp](/docs/channels/whatsapp) β Most popular; uses Baileys and requires QR pairing.
* [Telegram](/docs/channels/telegram) β Bot API via grammY; supports groups.
* [Discord](/docs/channels/discord) β Discord Bot API + Gateway; supports servers, channels, and DMs.
* [Slack](/docs/channels/slack) β Bolt SDK; workspace apps.
* [Google Chat](/docs/channels/googlechat) β Google Chat API app via HTTP webhook.
* [Mattermost](/docs/channels/mattermost) β Bot API + WebSocket; channels, groups, DMs (plugin, installed separately).
* [Signal](/docs/channels/signal) β signal-cli; privacy-focused.
* [BlueBubbles](/docs/channels/bluebubbles) β **Recommended for iMessage**; uses the BlueBubbles macOS server REST API with full feature support (edit, unsend, effects, reactions, group management β edit currently broken on macOS 26 Tahoe).
* [iMessage](/docs/channels/imessage) β macOS only; native integration via imsg (legacy, consider BlueBubbles for new setups).
* [Microsoft Teams](/docs/channels/msteams) β Bot Framework; enterprise support (plugin, installed separately).
* [Nextcloud Talk](/docs/channels/nextcloud-talk) β Self-hosted chat via Nextcloud Talk (plugin, installed separately).
* [Matrix](/docs/channels/matrix) β Matrix protocol (plugin, installed separately).
* [Nostr](/docs/channels/nostr) β Decentralized DMs via NIP-04 (plugin, installed separately).
* [Tlon](/docs/channels/tlon) β Urbit-based messenger (plugin, installed separately).
* [Zalo](/docs/channels/zalo) β Zalo Bot API; Vietnam's popular messenger (plugin, installed separately).
* [Zalo Personal](/docs/channels/zalouser) β Zalo personal account via QR login (plugin, installed separately).
* [WebChat](/docs/web/webchat) β Gateway WebChat UI over WebSocket.
Notes [#notes]
* Channels can run simultaneously; configure multiple and Reeve will route per chat.
* Fastest setup is usually **Telegram** (simple bot token). WhatsApp requires QR pairing and
stores more state on disk.
* Group behavior varies by channel; see [Groups](/docs/concepts/groups).
* DM pairing and allowlists are enforced for safety; see [Security](/docs/gateway/security).
* Telegram internals: [grammY notes](/docs/channels/grammy).
* Troubleshooting: [Channel troubleshooting](/docs/channels/troubleshooting).
* Model providers are documented separately; see [Model Providers](/docs/providers).
Next steps [#next-steps]
* [WhatsApp setup](/docs/channels/whatsapp) β Most popular channel, QR pairing
* [Telegram setup](/docs/channels/telegram) β Fastest to configure
* [Channel routing](/docs/concepts/channel-routing) β How messages reach agents
* [Broadcast groups](/docs/channels/broadcast-groups) β Send to multiple channels at once
# Channel location parsing (/docs/channels/location)
Channel location parsing [#channel-location-parsing]
Reeve normalizes shared locations from chat channels into:
* human-readable text appended to the inbound body, and
* structured fields in the auto-reply context payload.
Currently supported:
* **Telegram** (location pins + venues + live locations)
* **WhatsApp** (locationMessage + liveLocationMessage)
* **Matrix** (`m.location` with `geo_uri`)
Text formatting [#text-formatting]
Locations are rendered as friendly lines without brackets:
* Pin:
* `π 48.858844, 2.294351 Β±12m`
* Named place:
* `π Eiffel Tower β Champ de Mars, Paris (48.858844, 2.294351 Β±12m)`
* Live share:
* `π° Live location: 48.858844, 2.294351 Β±12m`
If the channel includes a caption/comment, it is appended on the next line:
```
π 48.858844, 2.294351 Β±12m
Meet here
```
Context fields [#context-fields]
When a location is present, these fields are added to `ctx`:
* `LocationLat` (number)
* `LocationLon` (number)
* `LocationAccuracy` (number, meters; optional)
* `LocationName` (string; optional)
* `LocationAddress` (string; optional)
* `LocationSource` (`pin | place | live`)
* `LocationIsLive` (boolean)
Channel notes [#channel-notes]
* **Telegram**: venues map to `LocationName/LocationAddress`; live locations use `live_period`.
* **WhatsApp**: `locationMessage.comment` and `liveLocationMessage.caption` are appended as the caption line.
* **Matrix**: `geo_uri` is parsed as a pin location; altitude is ignored and `LocationIsLive` is always false.
# Matrix (plugin) (/docs/channels/matrix)
Matrix (plugin) [#matrix-plugin]
Matrix is an open, decentralized messaging protocol. Reeve connects as a Matrix **user**
on any homeserver, so you need a Matrix account for the bot. Once it is logged in, you can DM
the bot directly or invite it to rooms (Matrix "groups"). Beeper is a valid client option too,
but it requires E2EE to be enabled.
Status: supported via plugin (matrix-bot-sdk). Direct messages, rooms, threads, media, reactions,
polls (send + poll-start as text), location, and E2EE (with crypto support).
Plugin required [#plugin-required]
Matrix ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
```bash
reeve plugins install @reeve/matrix
```
Local checkout (when running from a git repo):
```bash
reeve plugins install ./extensions/matrix
```
If you choose Matrix during configure/onboarding and a git checkout is detected,
Reeve will offer the local install path automatically.
Details: [Plugins](/plugin)
Setup [#setup]
1. Install the Matrix plugin:
* From npm: `reeve plugins install @reeve/matrix`
* From a local checkout: `reeve plugins install ./extensions/matrix`
2. Create a Matrix account on a homeserver:
* Browse hosting options at [https://matrix.org/ecosystem/hosting/](https://matrix.org/ecosystem/hosting/)
* Or host it yourself.
3. Get an access token for the bot account:
* Use the Matrix login API with `curl` at your home server:
```bash
curl --request POST \
--url https://matrix.example.org/_matrix/client/v3/login \
--header 'Content-Type: application/json' \
--data '{
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": "your-user-name"
},
"password": "your-password"
}'
```
* Replace `matrix.example.org` with your homeserver URL.
* Or set `channels.matrix.userId` + `channels.matrix.password`: Reeve calls the same
login endpoint, stores the access token in `~/.reeve/credentials/matrix/credentials.json`,
and reuses it on next start.
4. Configure credentials:
* Env: `MATRIX_HOMESERVER`, `MATRIX_ACCESS_TOKEN` (or `MATRIX_USER_ID` + `MATRIX_PASSWORD`)
* Or config: `channels.matrix.*`
* If both are set, config takes precedence.
* With access token: user ID is fetched automatically via `/whoami`.
* When set, `channels.matrix.userId` should be the full Matrix ID (example: `@bot:example.org`).
5. Restart the gateway (or finish onboarding).
6. Start a DM with the bot or invite it to a room from any Matrix client
(Element, Beeper, etc.; see [https://matrix.org/ecosystem/clients/](https://matrix.org/ecosystem/clients/)). Beeper requires E2EE,
so set `channels.matrix.encryption: true` and verify the device.
Minimal config (access token, user ID auto-fetched):
```json5
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
accessToken: "syt_***",
dm: { policy: "pairing" }
}
}
}
```
E2EE config (end to end encryption enabled):
```json5
{
channels: {
matrix: {
enabled: true,
homeserver: "https://matrix.example.org",
accessToken: "syt_***",
encryption: true,
dm: { policy: "pairing" }
}
}
}
```
Encryption (E2EE) [#encryption-e2ee]
End-to-end encryption is **supported** via the Rust crypto SDK.
Enable with `channels.matrix.encryption: true`:
* If the crypto module loads, encrypted rooms are decrypted automatically.
* Outbound media is encrypted when sending to encrypted rooms.
* On first connection, Reeve requests device verification from your other sessions.
* Verify the device in another Matrix client (Element, etc.) to enable key sharing.
* If the crypto module cannot be loaded, E2EE is disabled and encrypted rooms will not decrypt;
Reeve logs a warning.
* If you see missing crypto module errors (for example, `@matrix-org/matrix-sdk-crypto-nodejs-*`),
allow build scripts for `@matrix-org/matrix-sdk-crypto-nodejs` and run
`pnpm rebuild @matrix-org/matrix-sdk-crypto-nodejs` or fetch the binary with
`node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js`.
Crypto state is stored per account + access token in
`~/.reeve/matrix/accounts//__//crypto/`
(SQLite database). Sync state lives alongside it in `bot-storage.json`.
If the access token (device) changes, a new store is created and the bot must be
re-verified for encrypted rooms.
**Device verification:**
When E2EE is enabled, the bot will request verification from your other sessions on startup.
Open Element (or another client) and approve the verification request to establish trust.
Once verified, the bot can decrypt messages in encrypted rooms.
Routing model [#routing-model]
* Replies always go back to Matrix.
* DMs share the agent's main session; rooms map to group sessions.
Access control (DMs) [#access-control-dms]
* Default: `channels.matrix.dm.policy = "pairing"`. Unknown senders get a pairing code.
* Approve via:
* `reeve pairing list matrix`
* `reeve pairing approve matrix `
* Public DMs: `channels.matrix.dm.policy="open"` plus `channels.matrix.dm.allowFrom=["*"]`.
* `channels.matrix.dm.allowFrom` accepts user IDs or display names. The wizard resolves display names to user IDs when directory search is available.
Rooms (groups) [#rooms-groups]
* Default: `channels.matrix.groupPolicy = "allowlist"` (mention-gated). Use `channels.defaults.groupPolicy` to override the default when unset.
* Allowlist rooms with `channels.matrix.groups` (room IDs, aliases, or names):
```json5
{
channels: {
matrix: {
groupPolicy: "allowlist",
groups: {
"!roomId:example.org": { allow: true },
"#alias:example.org": { allow: true }
},
groupAllowFrom: ["@owner:example.org"]
}
}
}
```
* `requireMention: false` enables auto-reply in that room.
* `groups."*"` can set defaults for mention gating across rooms.
* `groupAllowFrom` restricts which senders can trigger the bot in rooms (optional).
* Per-room `users` allowlists can further restrict senders inside a specific room.
* The configure wizard prompts for room allowlists (room IDs, aliases, or names) and resolves names when possible.
* On startup, Reeve resolves room/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed.
* Invites are auto-joined by default; control with `channels.matrix.autoJoin` and `channels.matrix.autoJoinAllowlist`.
* To allow **no rooms**, set `channels.matrix.groupPolicy: "disabled"` (or keep an empty allowlist).
* Legacy key: `channels.matrix.rooms` (same shape as `groups`).
Threads [#threads]
* Reply threading is supported.
* `channels.matrix.threadReplies` controls whether replies stay in threads:
* `off`, `inbound` (default), `always`
* `channels.matrix.replyToMode` controls reply-to metadata when not replying in a thread:
* `off` (default), `first`, `all`
Capabilities [#capabilities]
| Feature | Status |
| --------------- | ------------------------------------------------------------------------------------ |
| Direct messages | β Supported |
| Rooms | β Supported |
| Threads | β Supported |
| Media | β Supported |
| E2EE | β Supported (crypto module required) |
| Reactions | β Supported (send/read via tools) |
| Polls | β Send supported; inbound poll starts are converted to text (responses/ends ignored) |
| Location | β Supported (geo URI; altitude ignored) |
| Native commands | β Supported |
Configuration reference (Matrix) [#configuration-reference-matrix]
Full configuration: [Configuration](/docs/gateway/configuration)
Provider options:
* `channels.matrix.enabled`: enable/disable channel startup.
* `channels.matrix.homeserver`: homeserver URL.
* `channels.matrix.userId`: Matrix user ID (optional with access token).
* `channels.matrix.accessToken`: access token.
* `channels.matrix.password`: password for login (token stored).
* `channels.matrix.deviceName`: device display name.
* `channels.matrix.encryption`: enable E2EE (default: false).
* `channels.matrix.initialSyncLimit`: initial sync limit.
* `channels.matrix.threadReplies`: `off | inbound | always` (default: inbound).
* `channels.matrix.textChunkLimit`: outbound text chunk size (chars).
* `channels.matrix.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
* `channels.matrix.dm.policy`: `pairing | allowlist | open | disabled` (default: pairing).
* `channels.matrix.dm.allowFrom`: DM allowlist (user IDs or display names). `open` requires `"*"`. The wizard resolves names to IDs when possible.
* `channels.matrix.groupPolicy`: `allowlist | open | disabled` (default: allowlist).
* `channels.matrix.groupAllowFrom`: allowlisted senders for group messages.
* `channels.matrix.allowlistOnly`: force allowlist rules for DMs + rooms.
* `channels.matrix.groups`: group allowlist + per-room settings map.
* `channels.matrix.rooms`: legacy group allowlist/config.
* `channels.matrix.replyToMode`: reply-to mode for threads/tags.
* `channels.matrix.mediaMaxMb`: inbound/outbound media cap (MB).
* `channels.matrix.autoJoin`: invite handling (`always | allowlist | off`, default: always).
* `channels.matrix.autoJoinAllowlist`: allowed room IDs/aliases for auto-join.
* `channels.matrix.actions`: per-action tool gating (reactions/messages/pins/memberInfo/channelInfo).
# Mattermost (plugin) (/docs/channels/mattermost)
Mattermost (plugin) [#mattermost-plugin]
Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported.
Mattermost is a self-hostable team messaging platform; see the official site at
[mattermost.com](https://mattermost.com) for product details and downloads.
Plugin required [#plugin-required]
Mattermost ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
```bash
reeve plugins install @reeve/mattermost
```
Local checkout (when running from a git repo):
```bash
reeve plugins install ./extensions/mattermost
```
If you choose Mattermost during configure/onboarding and a git checkout is detected,
Reeve will offer the local install path automatically.
Details: [Plugins](/plugin)
Quick setup [#quick-setup]
1. Install the Mattermost plugin.
2. Create a Mattermost bot account and copy the **bot token**.
3. Copy the Mattermost **base URL** (e.g., `https://chat.example.com`).
4. Configure Reeve and start the gateway.
Minimal config:
```json5
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing"
}
}
}
```
Environment variables (default account) [#environment-variables-default-account]
Set these on the gateway host if you prefer env vars:
* `MATTERMOST_BOT_TOKEN=...`
* `MATTERMOST_URL=https://chat.example.com`
Env vars apply only to the **default** account (`default`). Other accounts must use config values.
Chat modes [#chat-modes]
Mattermost responds to DMs automatically. Channel behavior is controlled by `chatmode`:
* `oncall` (default): respond only when @mentioned in channels.
* `onmessage`: respond to every channel message.
* `onchar`: respond when a message starts with a trigger prefix.
Config example:
```json5
{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"]
}
}
}
```
Notes:
* `onchar` still responds to explicit @mentions.
* `channels.mattermost.requireMention` is honored for legacy configs but `chatmode` is preferred.
Access control (DMs) [#access-control-dms]
* Default: `channels.mattermost.dmPolicy = "pairing"` (unknown senders get a pairing code).
* Approve via:
* `reeve pairing list mattermost`
* `reeve pairing approve mattermost `
* Public DMs: `channels.mattermost.dmPolicy="open"` plus `channels.mattermost.allowFrom=["*"]`.
Channels (groups) [#channels-groups]
* Default: `channels.mattermost.groupPolicy = "allowlist"` (mention-gated).
* Allowlist senders with `channels.mattermost.groupAllowFrom` (user IDs or `@username`).
* Open channels: `channels.mattermost.groupPolicy="open"` (mention-gated).
Targets for outbound delivery [#targets-for-outbound-delivery]
Use these target formats with `reeve message send` or cron/webhooks:
* `channel:` for a channel
* `user:` for a DM
* `@username` for a DM (resolved via the Mattermost API)
Bare IDs are treated as channels.
Multi-account [#multi-account]
Mattermost supports multiple accounts under `channels.mattermost.accounts`:
```json5
{
channels: {
mattermost: {
accounts: {
default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" },
alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" }
}
}
}
}
```
Troubleshooting [#troubleshooting]
* No replies in channels: ensure the bot is in the channel and mention it (oncall), use a trigger prefix (onchar), or set `chatmode: "onmessage"`.
* Auth errors: check the bot token, base URL, and whether the account is enabled.
* Multi-account issues: env vars only apply to the `default` account.
# Microsoft Teams (plugin) (/docs/channels/msteams)
Microsoft Teams (plugin) [#microsoft-teams-plugin]
> "Abandon all hope, ye who enter here."
Updated: 2026-01-21
Status: text + DM attachments are supported; channel/group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards.
Plugin required [#plugin-required]
Microsoft Teams ships as a plugin and is not bundled with the core install.
**Breaking change (2026.1.15):** MS Teams moved out of core. If you use it, you must install the plugin.
Explainable: keeps core installs lighter and lets MS Teams dependencies update independently.
Install via CLI (npm registry):
```bash
reeve plugins install @reeve/msteams
```
Local checkout (when running from a git repo):
```bash
reeve plugins install ./extensions/msteams
```
If you choose Teams during configure/onboarding and a git checkout is detected,
Reeve will offer the local install path automatically.
Details: [Plugins](/plugin)
Quick setup (beginner) [#quick-setup-beginner]
1. Install the Microsoft Teams plugin.
2. Create an **Azure Bot** (App ID + client secret + tenant ID).
3. Configure Reeve with those credentials.
4. Expose `/api/messages` (port 3978 by default) via a public URL or tunnel.
5. Install the Teams app package and start the gateway.
Minimal config:
```json5
{
channels: {
msteams: {
enabled: true,
appId: "",
appPassword: "",
tenantId: "",
webhook: { port: 3978, path: "/api/messages" }
}
}
}
```
Note: group chats are blocked by default (`channels.msteams.groupPolicy: "allowlist"`). To allow group replies, set `channels.msteams.groupAllowFrom` (or use `groupPolicy: "open"` to allow any member, mention-gated).
Goals [#goals]
* Talk to Reeve via Teams DMs, group chats, or channels.
* Keep routing deterministic: replies always go back to the channel they arrived on.
* Default to safe channel behavior (mentions required unless configured otherwise).
Config writes [#config-writes]
By default, Microsoft Teams is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
Disable with:
```json5
{
channels: { msteams: { configWrites: false } }
}
```
Access control (DMs + groups) [#access-control-dms--groups]
**DM access**
* Default: `channels.msteams.dmPolicy = "pairing"`. Unknown senders are ignored until approved.
* `channels.msteams.allowFrom` accepts AAD object IDs, UPNs, or display names. The wizard resolves names to IDs via Microsoft Graph when credentials allow.
**Group access**
* Default: `channels.msteams.groupPolicy = "allowlist"` (blocked unless you add `groupAllowFrom`). Use `channels.defaults.groupPolicy` to override the default when unset.
* `channels.msteams.groupAllowFrom` controls which senders can trigger in group chats/channels (falls back to `channels.msteams.allowFrom`).
* Set `groupPolicy: "open"` to allow any member (still mentionβgated by default).
* To allow **no channels**, set `channels.msteams.groupPolicy: "disabled"`.
Example:
```json5
{
channels: {
msteams: {
groupPolicy: "allowlist",
groupAllowFrom: ["user@org.com"]
}
}
}
```
**Teams + channel allowlist**
* Scope group/channel replies by listing teams and channels under `channels.msteams.teams`.
* Keys can be team IDs or names; channel keys can be conversation IDs or names.
* When `groupPolicy="allowlist"` and a teams allowlist is present, only listed teams/channels are accepted (mentionβgated).
* The configure wizard accepts `Team/Channel` entries and stores them for you.
* On startup, Reeve resolves team/channel and user allowlist names to IDs (when Graph permissions allow)
and logs the mapping; unresolved entries are kept as typed.
Example:
```json5
{
channels: {
msteams: {
groupPolicy: "allowlist",
teams: {
"My Team": {
channels: {
"General": { requireMention: true }
}
}
}
}
}
}
```
How it works [#how-it-works]
1. Install the Microsoft Teams plugin.
2. Create an **Azure Bot** (App ID + secret + tenant ID).
3. Build a **Teams app package** that references the bot and includes the RSC permissions below.
4. Upload/install the Teams app into a team (or personal scope for DMs).
5. Configure `msteams` in `~/.reeve/reeve.json` (or env vars) and start the gateway.
6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default.
Azure Bot Setup (Prerequisites) [#azure-bot-setup-prerequisites]
Before configuring Reeve, you need to create an Azure Bot resource.
Step 1: Create Azure Bot [#step-1-create-azure-bot]
1. Go to [Create Azure Bot](https://portal.azure.com/#create/Microsoft.AzureBot)
2. Fill in the **Basics** tab:
| Field | Value |
| ------------------ | ----------------------------------------------------- |
| **Bot handle** | Your bot name, e.g., `reeve-msteams` (must be unique) |
| **Subscription** | Select your Azure subscription |
| **Resource group** | Create new or use existing |
| **Pricing tier** | **Free** for dev/testing |
| **Type of App** | **Single Tenant** (recommended - see note below) |
| **Creation type** | **Create new Microsoft App ID** |
> **Deprecation notice:** Creation of new multi-tenant bots was deprecated after 2025-07-31. Use **Single Tenant** for new bots.
3. Click **Review + create** β **Create** (wait \~1-2 minutes)
Step 2: Get Credentials [#step-2-get-credentials]
1. Go to your Azure Bot resource β **Configuration**
2. Copy **Microsoft App ID** β this is your `appId`
3. Click **Manage Password** β go to the App Registration
4. Under **Certificates & secrets** β **New client secret** β copy the **Value** β this is your `appPassword`
5. Go to **Overview** β copy **Directory (tenant) ID** β this is your `tenantId`
Step 3: Configure Messaging Endpoint [#step-3-configure-messaging-endpoint]
1. In Azure Bot β **Configuration**
2. Set **Messaging endpoint** to your webhook URL:
* Production: `https://your-domain.com/api/messages`
* Local dev: Use a tunnel (see [Local Development](#local-development-tunneling) below)
Step 4: Enable Teams Channel [#step-4-enable-teams-channel]
1. In Azure Bot β **Channels**
2. Click **Microsoft Teams** β Configure β Save
3. Accept the Terms of Service
Local Development (Tunneling) [#local-development-tunneling]
Teams can't reach `localhost`. Use a tunnel for local development:
**Option A: ngrok**
```bash
ngrok http 3978
# Copy the https URL, e.g., https://abc123.ngrok.io
# Set messaging endpoint to: https://abc123.ngrok.io/api/messages
```
**Option B: Tailscale Funnel**
```bash
tailscale funnel 3978
# Use your Tailscale funnel URL as the messaging endpoint
```
Teams Developer Portal (Alternative) [#teams-developer-portal-alternative]
Instead of manually creating a manifest ZIP, you can use the [Teams Developer Portal](https://dev.teams.microsoft.com/apps):
1. Click **+ New app**
2. Fill in basic info (name, description, developer info)
3. Go to **App features** β **Bot**
4. Select **Enter a bot ID manually** and paste your Azure Bot App ID
5. Check scopes: **Personal**, **Team**, **Group Chat**
6. Click **Distribute** β **Download app package**
7. In Teams: **Apps** β **Manage your apps** β **Upload a custom app** β select the ZIP
This is often easier than hand-editing JSON manifests.
Testing the Bot [#testing-the-bot]
**Option A: Azure Web Chat (verify webhook first)**
1. In Azure Portal β your Azure Bot resource β **Test in Web Chat**
2. Send a message - you should see a response
3. This confirms your webhook endpoint works before Teams setup
**Option B: Teams (after app installation)**
1. Install the Teams app (sideload or org catalog)
2. Find the bot in Teams and send a DM
3. Check gateway logs for incoming activity
Setup (minimal text-only) [#setup-minimal-text-only]
1. **Install the Microsoft Teams plugin**
* From npm: `reeve plugins install @reeve/msteams`
* From a local checkout: `reeve plugins install ./extensions/msteams`
2. **Bot registration**
* Create an Azure Bot (see above) and note:
* App ID
* Client secret (App password)
* Tenant ID (single-tenant)
3. **Teams app manifest**
* Include a `bot` entry with `botId = `.
* Scopes: `personal`, `team`, `groupChat`.
* `supportsFiles: true` (required for personal scope file handling).
* Add RSC permissions (below).
* Create icons: `outline.png` (32x32) and `color.png` (192x192).
* Zip all three files together: `manifest.json`, `outline.png`, `color.png`.
4. **Configure Reeve**
```json
{
"msteams": {
"enabled": true,
"appId": "",
"appPassword": "",
"tenantId": "",
"webhook": { "port": 3978, "path": "/api/messages" }
}
}
```
You can also use environment variables instead of config keys:
* `MSTEAMS_APP_ID`
* `MSTEAMS_APP_PASSWORD`
* `MSTEAMS_TENANT_ID`
5. **Bot endpoint**
* Set the Azure Bot Messaging Endpoint to:
* `https://:3978/api/messages` (or your chosen path/port).
6. **Run the gateway**
* The Teams channel starts automatically when the plugin is installed and `msteams` config exists with credentials.
History context [#history-context]
* `channels.msteams.historyLimit` controls how many recent channel/group messages are wrapped into the prompt.
* Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
* DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms[""].historyLimit`.
Current Teams RSC Permissions (Manifest) [#current-teams-rsc-permissions-manifest]
These are the **existing resourceSpecific permissions** in our Teams app manifest. They only apply inside the team/chat where the app is installed.
**For channels (team scope):**
* `ChannelMessage.Read.Group` (Application) - receive all channel messages without @mention
* `ChannelMessage.Send.Group` (Application)
* `Member.Read.Group` (Application)
* `Owner.Read.Group` (Application)
* `ChannelSettings.Read.Group` (Application)
* `TeamMember.Read.Group` (Application)
* `TeamSettings.Read.Group` (Application)
**For group chats:**
* `ChatMessage.Read.Chat` (Application) - receive all group chat messages without @mention
Example Teams Manifest (redacted) [#example-teams-manifest-redacted]
Minimal, valid example with the required fields. Replace IDs and URLs.
```json
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
"manifestVersion": "1.23",
"version": "1.0.0",
"id": "00000000-0000-0000-0000-000000000000",
"name": { "short": "Reeve" },
"developer": {
"name": "Your Org",
"websiteUrl": "https://example.com",
"privacyUrl": "https://example.com/privacy",
"termsOfUseUrl": "https://example.com/terms"
},
"description": { "short": "Reeve in Teams", "full": "Reeve in Teams" },
"icons": { "outline": "outline.png", "color": "color.png" },
"accentColor": "#5B6DEF",
"bots": [
{
"botId": "11111111-1111-1111-1111-111111111111",
"scopes": ["personal", "team", "groupChat"],
"isNotificationOnly": false,
"supportsCalling": false,
"supportsVideo": false,
"supportsFiles": true
}
],
"webApplicationInfo": {
"id": "11111111-1111-1111-1111-111111111111"
},
"authorization": {
"permissions": {
"resourceSpecific": [
{ "name": "ChannelMessage.Read.Group", "type": "Application" },
{ "name": "ChannelMessage.Send.Group", "type": "Application" },
{ "name": "Member.Read.Group", "type": "Application" },
{ "name": "Owner.Read.Group", "type": "Application" },
{ "name": "ChannelSettings.Read.Group", "type": "Application" },
{ "name": "TeamMember.Read.Group", "type": "Application" },
{ "name": "TeamSettings.Read.Group", "type": "Application" },
{ "name": "ChatMessage.Read.Chat", "type": "Application" }
]
}
}
}
```
Manifest caveats (must-have fields) [#manifest-caveats-must-have-fields]
* `bots[].botId` **must** match the Azure Bot App ID.
* `webApplicationInfo.id` **must** match the Azure Bot App ID.
* `bots[].scopes` must include the surfaces you plan to use (`personal`, `team`, `groupChat`).
* `bots[].supportsFiles: true` is required for file handling in personal scope.
* `authorization.permissions.resourceSpecific` must include channel read/send if you want channel traffic.
Updating an existing app [#updating-an-existing-app]
To update an already-installed Teams app (e.g., to add RSC permissions):
1. Update your `manifest.json` with the new settings
2. **Increment the `version` field** (e.g., `1.0.0` β `1.1.0`)
3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`)
4. Upload the new zip:
* **Option A (Teams Admin Center):** Teams Admin Center β Teams apps β Manage apps β find your app β Upload new version
* **Option B (Sideload):** In Teams β Apps β Manage your apps β Upload a custom app
5. **For team channels:** Reinstall the app in each team for new permissions to take effect
6. **Fully quit and relaunch Teams** (not just close the window) to clear cached app metadata
Capabilities: RSC only vs Graph [#capabilities-rsc-only-vs-graph]
With Teams RSC only (app installed, no Graph API permissions) [#with-teams-rsc-only-app-installed-no-graph-api-permissions]
Works:
* Read channel message **text** content.
* Send channel message **text** content.
* Receive **personal (DM)** file attachments.
Does NOT work:
* Channel/group **image or file contents** (payload only includes HTML stub).
* Downloading attachments stored in SharePoint/OneDrive.
* Reading message history (beyond the live webhook event).
With Teams RSC + Microsoft Graph Application permissions [#with-teams-rsc--microsoft-graph-application-permissions]
Adds:
* Downloading hosted contents (images pasted into messages).
* Downloading file attachments stored in SharePoint/OneDrive.
* Reading channel/chat message history via Graph.
RSC vs Graph API [#rsc-vs-graph-api]
| Capability | RSC Permissions | Graph API |
| ----------------------- | -------------------- | ----------------------------------- |
| **Real-time messages** | Yes (via webhook) | No (polling only) |
| **Historical messages** | No | Yes (can query history) |
| **Setup complexity** | App manifest only | Requires admin consent + token flow |
| **Works offline** | No (must be running) | Yes (query anytime) |
**Bottom line:** RSC is for real-time listening; Graph API is for historical access. For catching up on missed messages while offline, you need Graph API with `ChannelMessage.Read.All` (requires admin consent).
Graph-enabled media + history (required for channels) [#graph-enabled-media--history-required-for-channels]
If you need images/files in **channels** or want to fetch **message history**, you must enable Microsoft Graph permissions and grant admin consent.
1. In Entra ID (Azure AD) **App Registration**, add Microsoft Graph **Application permissions**:
* `ChannelMessage.Read.All` (channel attachments + history)
* `Chat.Read.All` or `ChatMessage.Read.All` (group chats)
2. **Grant admin consent** for the tenant.
3. Bump the Teams app **manifest version**, re-upload, and **reinstall the app in Teams**.
4. **Fully quit and relaunch Teams** to clear cached app metadata.
Known Limitations [#known-limitations]
Webhook timeouts [#webhook-timeouts]
Teams delivers messages via HTTP webhook. If processing takes too long (e.g., slow LLM responses), you may see:
* Gateway timeouts
* Teams retrying the message (causing duplicates)
* Dropped replies
Reeve handles this by returning quickly and sending replies proactively, but very slow responses may still cause issues.
Formatting [#formatting]
Teams markdown is more limited than Slack or Discord:
* Basic formatting works: **bold**, *italic*, `code`, links
* Complex markdown (tables, nested lists) may not render correctly
* Adaptive Cards are supported for polls and arbitrary card sends (see below)
Configuration [#configuration]
Key settings (see `/gateway/configuration` for shared channel patterns):
* `channels.msteams.enabled`: enable/disable the channel.
* `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials.
* `channels.msteams.webhook.port` (default `3978`)
* `channels.msteams.webhook.path` (default `/api/messages`)
* `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
* `channels.msteams.allowFrom`: allowlist for DMs (AAD object IDs, UPNs, or display names). The wizard resolves names to IDs during setup when Graph access is available.
* `channels.msteams.textChunkLimit`: outbound text chunk size.
* `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
* `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
* `channels.msteams.requireMention`: require @mention in channels/groups (default true).
* `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
* `channels.msteams.teams..replyStyle`: per-team override.
* `channels.msteams.teams..requireMention`: per-team override.
* `channels.msteams.teams..channels..replyStyle`: per-channel override.
* `channels.msteams.teams..channels..requireMention`: per-channel override.
* `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)).
Routing & Sessions [#routing--sessions]
* Session keys follow the standard agent format (see [/concepts/session](/docs/concepts/session)):
* Direct messages share the main session (`agent::`).
* Channel/group messages use conversation id:
* `agent::msteams:channel:`
* `agent::msteams:group:`
Reply Style: Threads vs Posts [#reply-style-threads-vs-posts]
Teams recently introduced two channel UI styles over the same underlying data model:
| Style | Description | Recommended `replyStyle` |
| ------------------------ | --------------------------------------------------------- | ------------------------ |
| **Posts** (classic) | Messages appear as cards with threaded replies underneath | `thread` (default) |
| **Threads** (Slack-like) | Messages flow linearly, more like Slack | `top-level` |
**The problem:** The Teams API does not expose which UI style a channel uses. If you use the wrong `replyStyle`:
* `thread` in a Threads-style channel β replies appear nested awkwardly
* `top-level` in a Posts-style channel β replies appear as separate top-level posts instead of in-thread
**Solution:** Configure `replyStyle` per-channel based on how the channel is set up:
```json
{
"msteams": {
"replyStyle": "thread",
"teams": {
"19:abc...@thread.tacv2": {
"channels": {
"19:xyz...@thread.tacv2": {
"replyStyle": "top-level"
}
}
}
}
}
}
```
Attachments & Images [#attachments--images]
**Current limitations:**
* **DMs:** Images and file attachments work via Teams bot file APIs.
* **Channels/groups:** Attachments live in M365 storage (SharePoint/OneDrive). The webhook payload only includes an HTML stub, not the actual file bytes. **Graph API permissions are required** to download channel attachments.
Without Graph permissions, channel messages with images will be received as text-only (the image content is not accessible to the bot).
By default, Reeve only downloads media from Microsoft/Teams hostnames. Override with `channels.msteams.mediaAllowHosts` (use `["*"]` to allow any host).
Sending files in group chats [#sending-files-in-group-chats]
Bots can send files in DMs using the FileConsentCard flow (built-in). However, **sending files in group chats/channels** requires additional setup:
| Context | How files are sent | Setup needed |
| ------------------------ | -------------------------------------------- | ----------------------------------------------- |
| **DMs** | FileConsentCard β user accepts β bot uploads | Works out of the box |
| **Group chats/channels** | Upload to SharePoint β share link | Requires `sharePointSiteId` + Graph permissions |
| **Images (any context)** | Base64-encoded inline | Works out of the box |
Why group chats need SharePoint [#why-group-chats-need-sharepoint]
Bots don't have a personal OneDrive drive (the `/me/drive` Graph API endpoint doesn't work for application identities). To send files in group chats/channels, the bot uploads to a **SharePoint site** and creates a sharing link.
Setup [#setup]
1. **Add Graph API permissions** in Entra ID (Azure AD) β App Registration:
* `Sites.ReadWrite.All` (Application) - upload files to SharePoint
* `Chat.Read.All` (Application) - optional, enables per-user sharing links
2. **Grant admin consent** for the tenant.
3. **Get your SharePoint site ID:**
```bash
# Via Graph Explorer or curl with a valid token:
curl -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/sites/\{hostname\}:/{site-path}"
# Example: for a site at "contoso.sharepoint.com/sites/BotFiles"
curl -H "Authorization: Bearer $TOKEN" \
"https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com:/sites/BotFiles"
# Response includes: "id": "contoso.sharepoint.com,guid1,guid2"
```
4. **Configure Reeve:**
```json5
{
channels: {
msteams: {
// ... other config ...
sharePointSiteId: "contoso.sharepoint.com,guid1,guid2"
}
}
}
```
Sharing behavior [#sharing-behavior]
| Permission | Sharing behavior |
| --------------------------------------- | --------------------------------------------------------- |
| `Sites.ReadWrite.All` only | Organization-wide sharing link (anyone in org can access) |
| `Sites.ReadWrite.All` + `Chat.Read.All` | Per-user sharing link (only chat members can access) |
Per-user sharing is more secure as only the chat participants can access the file. If `Chat.Read.All` permission is missing, the bot falls back to organization-wide sharing.
Fallback behavior [#fallback-behavior]
| Scenario | Result |
| ------------------------------------------------- | -------------------------------------------------- |
| Group chat + file + `sharePointSiteId` configured | Upload to SharePoint, send sharing link |
| Group chat + file + no `sharePointSiteId` | Attempt OneDrive upload (may fail), send text only |
| Personal chat + file | FileConsentCard flow (works without SharePoint) |
| Any context + image | Base64-encoded inline (works without SharePoint) |
Files stored location [#files-stored-location]
Uploaded files are stored in a `/ReeveShared/` folder in the configured SharePoint site's default document library.
Polls (Adaptive Cards) [#polls-adaptive-cards]
Reeve sends Teams polls as Adaptive Cards (there is no native Teams poll API).
* CLI: `reeve message poll --channel msteams --target conversation: ...`
* Votes are recorded by the gateway in `~/.reeve/msteams-polls.json`.
* The gateway must stay online to record votes.
* Polls do not auto-post result summaries yet (inspect the store file if needed).
Adaptive Cards (arbitrary) [#adaptive-cards-arbitrary]
Send any Adaptive Card JSON to Teams users or conversations using the `message` tool or CLI.
The `card` parameter accepts an Adaptive Card JSON object. When `card` is provided, the message text is optional.
**Agent tool:**
```json
{
"action": "send",
"channel": "msteams",
"target": "user:",
"card": {
"type": "AdaptiveCard",
"version": "1.5",
"body": [{"type": "TextBlock", "text": "Hello!"}]
}
}
```
**CLI:**
```bash
reeve message send --channel msteams \
--target "conversation:19:abc...@thread.tacv2" \
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello!"}]}'
```
See [Adaptive Cards documentation](https://adaptivecards.io/) for card schema and examples. For target format details, see [Target formats](#target-formats) below.
Target formats [#target-formats]
MSTeams targets use prefixes to distinguish between users and conversations:
| Target type | Format | Example |
| ------------------- | -------------------------------- | --------------------------------------------------- |
| User (by ID) | `user:` | `user:40a1a0ed-4ff2-4164-a219-55518990c197` |
| User (by name) | `user:` | `user:John Smith` (requires Graph API) |
| Group/channel | `conversation:` | `conversation:19:abc123...@thread.tacv2` |
| Group/channel (raw) | `` | `19:abc123...@thread.tacv2` (if contains `@thread`) |
**CLI examples:**
```bash
# Send to a user by ID
reeve message send --channel msteams --target "user:40a1a0ed-..." --message "Hello"
# Send to a user by display name (triggers Graph API lookup)
reeve message send --channel msteams --target "user:John Smith" --message "Hello"
# Send to a group chat or channel
reeve message send --channel msteams --target "conversation:19:abc...@thread.tacv2" --message "Hello"
# Send an Adaptive Card to a conversation
reeve message send --channel msteams --target "conversation:19:abc...@thread.tacv2" \
--card '{"type":"AdaptiveCard","version":"1.5","body":[{"type":"TextBlock","text":"Hello"}]}'
```
**Agent tool examples:**
```json
{
"action": "send",
"channel": "msteams",
"target": "user:John Smith",
"message": "Hello!"
}
```
```json
{
"action": "send",
"channel": "msteams",
"target": "conversation:19:abc...@thread.tacv2",
"card": {"type": "AdaptiveCard", "version": "1.5", "body": [{"type": "TextBlock", "text": "Hello"}]}
}
```
Note: Without the `user:` prefix, names default to group/team resolution. Always use `user:` when targeting people by display name.
Proactive messaging [#proactive-messaging]
* Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point.
* See `/gateway/configuration` for `dmPolicy` and allowlist gating.
Team and Channel IDs (Common Gotcha) [#team-and-channel-ids-common-gotcha]
The `groupId` query parameter in Teams URLs is **NOT** the team ID used for configuration. Extract IDs from the URL path instead:
**Team URL:**
```
https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?groupId=...
ββββββββββββββββββββββββββββββ
Team ID (URL-decode this)
```
**Channel URL:**
```
https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?groupId=...
βββββββββββββββββββββββββββ
Channel ID (URL-decode this)
```
**For config:**
* Team ID = path segment after `/team/` (URL-decoded, e.g., `19:Bk4j...@thread.tacv2`)
* Channel ID = path segment after `/channel/` (URL-decoded)
* **Ignore** the `groupId` query parameter
Private Channels [#private-channels]
Bots have limited support in private channels:
| Feature | Standard Channels | Private Channels |
| ---------------------------- | ----------------- | ---------------------- |
| Bot installation | Yes | Limited |
| Real-time messages (webhook) | Yes | May not work |
| RSC permissions | Yes | May behave differently |
| @mentions | Yes | If bot is accessible |
| Graph API history | Yes | Yes (with permissions) |
**Workarounds if private channels don't work:**
1. Use standard channels for bot interactions
2. Use DMs - users can always message the bot directly
3. Use Graph API for historical access (requires `ChannelMessage.Read.All`)
Troubleshooting [#troubleshooting]
Common issues [#common-issues]
* **Images not showing in channels:** Graph permissions or admin consent missing. Reinstall the Teams app and fully quit/reopen Teams.
* **No responses in channel:** mentions are required by default; set `channels.msteams.requireMention=false` or configure per team/channel.
* **Version mismatch (Teams still shows old manifest):** remove + re-add the app and fully quit Teams to refresh.
* **401 Unauthorized from webhook:** Expected when testing manually without Azure JWT - means endpoint is reachable but auth failed. Use Azure Web Chat to test properly.
Manifest upload errors [#manifest-upload-errors]
* **"Icon file cannot be empty":** The manifest references icon files that are 0 bytes. Create valid PNG icons (32x32 for `outline.png`, 192x192 for `color.png`).
* **"webApplicationInfo.Id already in use":** The app is still installed in another team/chat. Find and uninstall it first, or wait 5-10 minutes for propagation.
* **"Something went wrong" on upload:** Upload via [https://admin.teams.microsoft.com](https://admin.teams.microsoft.com) instead, open browser DevTools (F12) β Network tab, and check the response body for the actual error.
* **Sideload failing:** Try "Upload an app to your org's app catalog" instead of "Upload a custom app" - this often bypasses sideload restrictions.
RSC permissions not working [#rsc-permissions-not-working]
1. Verify `webApplicationInfo.id` matches your bot's App ID exactly
2. Re-upload the app and reinstall in the team/chat
3. Check if your org admin has blocked RSC permissions
4. Confirm you're using the right scope: `ChannelMessage.Read.Group` for teams, `ChatMessage.Read.Chat` for group chats
References [#references]
* [Create Azure Bot](https://learn.microsoft.com/en-us/azure/bot-service/bot-service-quickstart-registration) - Azure Bot setup guide
* [Teams Developer Portal](https://dev.teams.microsoft.com/apps) - create/manage Teams apps
* [Teams app manifest schema](https://learn.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema)
* [Receive channel messages with RSC](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/channel-messages-with-rsc)
* [RSC permissions reference](https://learn.microsoft.com/en-us/microsoftteams/platform/graph-api/rsc/resource-specific-consent)
* [Teams bot file handling](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) (channel/group requires Graph)
* [Proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages)
# Nextcloud Talk (plugin) (/docs/channels/nextcloud-talk)
Nextcloud Talk (plugin) [#nextcloud-talk-plugin]
Status: supported via plugin (webhook bot). Direct messages, rooms, reactions, and markdown messages are supported.
Plugin required [#plugin-required]
Nextcloud Talk ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
```bash
reeve plugins install @reeve/nextcloud-talk
```
Local checkout (when running from a git repo):
```bash
reeve plugins install ./extensions/nextcloud-talk
```
If you choose Nextcloud Talk during configure/onboarding and a git checkout is detected,
Reeve will offer the local install path automatically.
Details: [Plugins](/plugin)
Quick setup (beginner) [#quick-setup-beginner]
1. Install the Nextcloud Talk plugin.
2. On your Nextcloud server, create a bot:
```bash
./occ talk:bot:install "Reeve" "" "" --feature reaction
```
3. Enable the bot in the target room settings.
4. Configure Reeve:
* Config: `channels.nextcloud-talk.baseUrl` + `channels.nextcloud-talk.botSecret`
* Or env: `NEXTCLOUD_TALK_BOT_SECRET` (default account only)
5. Restart the gateway (or finish onboarding).
Minimal config:
```json5
{
channels: {
"nextcloud-talk": {
enabled: true,
baseUrl: "https://cloud.example.com",
botSecret: "shared-secret",
dmPolicy: "pairing"
}
}
}
```
Notes [#notes]
* Bots cannot initiate DMs. The user must message the bot first.
* Webhook URL must be reachable by the Gateway; set `webhookPublicUrl` if behind a proxy.
* Media uploads are not supported by the bot API; media is sent as URLs.
* The webhook payload does not distinguish DMs vs rooms; set `apiUser` + `apiPassword` to enable room-type lookups (otherwise DMs are treated as rooms).
Access control (DMs) [#access-control-dms]
* Default: `channels.nextcloud-talk.dmPolicy = "pairing"`. Unknown senders get a pairing code.
* Approve via:
* `reeve pairing list nextcloud-talk`
* `reeve pairing approve nextcloud-talk `
* Public DMs: `channels.nextcloud-talk.dmPolicy="open"` plus `channels.nextcloud-talk.allowFrom=["*"]`.
Rooms (groups) [#rooms-groups]
* Default: `channels.nextcloud-talk.groupPolicy = "allowlist"` (mention-gated).
* Allowlist rooms with `channels.nextcloud-talk.rooms`:
```json5
{
channels: {
"nextcloud-talk": {
rooms: {
"room-token": { requireMention: true }
}
}
}
}
```
* To allow no rooms, keep the allowlist empty or set `channels.nextcloud-talk.groupPolicy="disabled"`.
Capabilities [#capabilities]
| Feature | Status |
| --------------- | ------------- |
| Direct messages | Supported |
| Rooms | Supported |
| Threads | Not supported |
| Media | URL-only |
| Reactions | Supported |
| Native commands | Not supported |
Configuration reference (Nextcloud Talk) [#configuration-reference-nextcloud-talk]
Full configuration: [Configuration](/docs/gateway/configuration)
Provider options:
* `channels.nextcloud-talk.enabled`: enable/disable channel startup.
* `channels.nextcloud-talk.baseUrl`: Nextcloud instance URL.
* `channels.nextcloud-talk.botSecret`: bot shared secret.
* `channels.nextcloud-talk.botSecretFile`: secret file path.
* `channels.nextcloud-talk.apiUser`: API user for room lookups (DM detection).
* `channels.nextcloud-talk.apiPassword`: API/app password for room lookups.
* `channels.nextcloud-talk.apiPasswordFile`: API password file path.
* `channels.nextcloud-talk.webhookPort`: webhook listener port (default: 8788).
* `channels.nextcloud-talk.webhookHost`: webhook host (default: 0.0.0.0).
* `channels.nextcloud-talk.webhookPath`: webhook path (default: /nextcloud-talk-webhook).
* `channels.nextcloud-talk.webhookPublicUrl`: externally reachable webhook URL.
* `channels.nextcloud-talk.dmPolicy`: `pairing | allowlist | open | disabled`.
* `channels.nextcloud-talk.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`.
* `channels.nextcloud-talk.groupPolicy`: `allowlist | open | disabled`.
* `channels.nextcloud-talk.groupAllowFrom`: group allowlist (user IDs).
* `channels.nextcloud-talk.rooms`: per-room settings and allowlist.
* `channels.nextcloud-talk.historyLimit`: group history limit (0 disables).
* `channels.nextcloud-talk.dmHistoryLimit`: DM history limit (0 disables).
* `channels.nextcloud-talk.dms`: per-DM overrides (historyLimit).
* `channels.nextcloud-talk.textChunkLimit`: outbound text chunk size (chars).
* `channels.nextcloud-talk.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
* `channels.nextcloud-talk.blockStreaming`: disable block streaming for this channel.
* `channels.nextcloud-talk.blockStreamingCoalesce`: block streaming coalesce tuning.
* `channels.nextcloud-talk.mediaMaxMb`: inbound media cap (MB).
# Nostr (/docs/channels/nostr)
Nostr [#nostr]
**Status:** Optional plugin (disabled by default).
Nostr is a decentralized protocol for social networking. This channel enables Reeve to receive and respond to encrypted direct messages (DMs) via NIP-04.
Install (on demand) [#install-on-demand]
Onboarding (recommended) [#onboarding-recommended]
* The onboarding wizard (`reeve onboard`) and `reeve channels add` list optional channel plugins.
* Selecting Nostr prompts you to install the plugin on demand.
Install defaults:
* **Dev channel + git checkout available:** uses the local plugin path.
* **Stable/Beta:** downloads from npm.
You can always override the choice in the prompt.
Manual install [#manual-install]
```bash
reeve plugins install @reeve/nostr
```
Use a local checkout (dev workflows):
```bash
reeve plugins install --link /extensions/nostr
```
Restart the Gateway after installing or enabling plugins.
Quick setup [#quick-setup]
1. Generate a Nostr keypair (if needed):
```bash
# Using nak
nak key generate
```
2. Add to config:
```json
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}"
}
}
}
```
3. Export the key:
```bash
export NOSTR_PRIVATE_KEY="nsec1..."
```
4. Restart the Gateway.
Configuration reference [#configuration-reference]
| Key | Type | Default | Description |
| ------------ | --------- | ------------------------------------------- | ----------------------------------- |
| `privateKey` | string | required | Private key in `nsec` or hex format |
| `relays` | string\[] | `['wss://relay.damus.io', 'wss://nos.lol']` | Relay URLs (WebSocket) |
| `dmPolicy` | string | `pairing` | DM access policy |
| `allowFrom` | string\[] | `[]` | Allowed sender pubkeys |
| `enabled` | boolean | `true` | Enable/disable channel |
| `name` | string | - | Display name |
| `profile` | object | - | NIP-01 profile metadata |
Profile metadata [#profile-metadata]
Profile data is published as a NIP-01 `kind:0` event. You can manage it from the Control UI (Channels -> Nostr -> Profile) or set it directly in config.
Example:
```json
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}",
"profile": {
"name": "reeve",
"displayName": "Reeve",
"about": "Personal assistant DM bot",
"picture": "https://example.com/avatar.png",
"banner": "https://example.com/banner.png",
"website": "https://example.com",
"nip05": "reeve@example.com",
"lud16": "reeve@example.com"
}
}
}
}
```
Notes:
* Profile URLs must use `https://`.
* Importing from relays merges fields and preserves local overrides.
Access control [#access-control]
DM policies [#dm-policies]
* **pairing** (default): unknown senders get a pairing code.
* **allowlist**: only pubkeys in `allowFrom` can DM.
* **open**: public inbound DMs (requires `allowFrom: ["*"]`).
* **disabled**: ignore inbound DMs.
Allowlist example [#allowlist-example]
```json
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}",
"dmPolicy": "allowlist",
"allowFrom": ["npub1abc...", "npub1xyz..."]
}
}
}
```
Key formats [#key-formats]
Accepted formats:
* **Private key:** `nsec...` or 64-char hex
* **Pubkeys (`allowFrom`):** `npub...` or hex
Relays [#relays]
Defaults: `relay.damus.io` and `nos.lol`.
```json
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}",
"relays": [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://nostr.wine"
]
}
}
}
```
Tips:
* Use 2-3 relays for redundancy.
* Avoid too many relays (latency, duplication).
* Paid relays can improve reliability.
* Local relays are fine for testing (`ws://localhost:7777`).
Protocol support [#protocol-support]
| NIP | Status | Description |
| ------ | --------- | ------------------------------------- |
| NIP-01 | Supported | Basic event format + profile metadata |
| NIP-04 | Supported | Encrypted DMs (`kind:4`) |
| NIP-17 | Planned | Gift-wrapped DMs |
| NIP-44 | Planned | Versioned encryption |
Testing [#testing]
Local relay [#local-relay]
```bash
# Start strfry
docker run -p 7777:7777 ghcr.io/hoytech/strfry
```
```json
{
"channels": {
"nostr": {
"privateKey": "${NOSTR_PRIVATE_KEY}",
"relays": ["ws://localhost:7777"]
}
}
}
```
Manual test [#manual-test]
1. Note the bot pubkey (npub) from logs.
2. Open a Nostr client (Damus, Amethyst, etc.).
3. DM the bot pubkey.
4. Verify the response.
Troubleshooting [#troubleshooting]
Not receiving messages [#not-receiving-messages]
* Verify the private key is valid.
* Ensure relay URLs are reachable and use `wss://` (or `ws://` for local).
* Confirm `enabled` is not `false`.
* Check Gateway logs for relay connection errors.
Not sending responses [#not-sending-responses]
* Check relay accepts writes.
* Verify outbound connectivity.
* Watch for relay rate limits.
Duplicate responses [#duplicate-responses]
* Expected when using multiple relays.
* Messages are deduplicated by event ID; only the first delivery triggers a response.
Security [#security]
* Never commit private keys.
* Use environment variables for keys.
* Consider `allowlist` for production bots.
Limitations (MVP) [#limitations-mvp]
* Direct messages only (no group chats).
* No media attachments.
* NIP-04 only (NIP-17 gift-wrap planned).
# Signal (signal-cli) (/docs/channels/signal)
Signal (signal-cli) [#signal-signal-cli]
Status: external CLI integration. Gateway talks to `signal-cli` over HTTP JSON-RPC + SSE.
Quick setup (beginner) [#quick-setup-beginner]
1. Use a **separate Signal number** for the bot (recommended).
2. Install `signal-cli` (Java required).
3. Link the bot device and start the daemon:
* `signal-cli link -n "Reeve"`
4. Configure Reeve and start the gateway.
Minimal config:
```json5
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"]
}
}
}
```
What it is [#what-it-is]
* Signal channel via `signal-cli` (not embedded libsignal).
* Deterministic routing: replies always go back to Signal.
* DMs share the agent's main session; groups are isolated (`agent::signal:group:`).
Config writes [#config-writes]
By default, Signal is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
Disable with:
```json5
{
channels: { signal: { configWrites: false } }
}
```
The number model (important) [#the-number-model-important]
* The gateway connects to a **Signal device** (the `signal-cli` account).
* If you run the bot on **your personal Signal account**, it will ignore your own messages (loop protection).
* For "I text the bot and it replies," use a **separate bot number**.
Setup (fast path) [#setup-fast-path]
1. Install `signal-cli` (Java required).
2. Link a bot account:
* `signal-cli link -n "Reeve"` then scan the QR in Signal.
3. Configure Signal and start the gateway.
Example:
```json5
{
channels: {
signal: {
enabled: true,
account: "+15551234567",
cliPath: "signal-cli",
dmPolicy: "pairing",
allowFrom: ["+15557654321"]
}
}
}
```
Multi-account support: use `channels.signal.accounts` with per-account config and optional `name`. See [`gateway/configuration`](/docs/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
External daemon mode (httpUrl) [#external-daemon-mode-httpurl]
If you want to manage `signal-cli` yourself (slow JVM cold starts, container init, or shared CPUs), run the daemon separately and point Reeve at it:
```json5
{
channels: {
signal: {
httpUrl: "http://127.0.0.1:8080",
autoStart: false
}
}
}
```
This skips auto-spawn and the startup wait inside Reeve. For slow starts when auto-spawning, set `channels.signal.startupTimeoutMs`.
Access control (DMs + groups) [#access-control-dms--groups]
DMs:
* Default: `channels.signal.dmPolicy = "pairing"`.
* Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
* Approve via:
* `reeve pairing list signal`
* `reeve pairing approve signal `
* Pairing is the default token exchange for Signal DMs. Details: [Pairing](/docs/start/pairing)
* UUID-only senders (from `sourceUuid`) are stored as `uuid:` in `channels.signal.allowFrom`.
Groups:
* `channels.signal.groupPolicy = open | allowlist | disabled`.
* `channels.signal.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
How it works (behavior) [#how-it-works-behavior]
* `signal-cli` runs as a daemon; the gateway reads events via SSE.
* Inbound messages are normalized into the shared channel envelope.
* Replies always route back to the same number or group.
Media + limits [#media--limits]
* Outbound text is chunked to `channels.signal.textChunkLimit` (default 4000).
* Optional newline chunking: set `channels.signal.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
* Attachments supported (base64 fetched from `signal-cli`).
* Default media cap: `channels.signal.mediaMaxMb` (default 8).
* Use `channels.signal.ignoreAttachments` to skip downloading media.
* Group history context uses `channels.signal.historyLimit` (or `channels.signal.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
Typing + read receipts [#typing--read-receipts]
* **Typing indicators**: Reeve sends typing signals via `signal-cli sendTyping` and refreshes them while a reply is running.
* **Read receipts**: when `channels.signal.sendReadReceipts` is true, Reeve forwards read receipts for allowed DMs.
* Signal-cli does not expose read receipts for groups.
Reactions (message tool) [#reactions-message-tool]
* Use `message action=react` with `channel=signal`.
* Targets: sender E.164 or UUID (use `uuid:` from pairing output; bare UUID works too).
* `messageId` is the Signal timestamp for the message youβre reacting to.
* Group reactions require `targetAuthor` or `targetAuthorUuid`.
Examples:
```
message action=react channel=signal target=uuid:123e4567-e89b-12d3-a456-426614174000 messageId=1737630212345 emoji=π₯
message action=react channel=signal target=+15551234567 messageId=1737630212345 emoji=π₯ remove=true
message action=react channel=signal target=signal:group: targetAuthor=uuid: messageId=1737630212345 emoji=β
```
Config:
* `channels.signal.actions.reactions`: enable/disable reaction actions (default true).
* `channels.signal.reactionLevel`: `off | ack | minimal | extensive`.
* `off`/`ack` disables agent reactions (message tool `react` will error).
* `minimal`/`extensive` enables agent reactions and sets the guidance level.
* Per-account overrides: `channels.signal.accounts..actions.reactions`, `channels.signal.accounts..reactionLevel`.
Delivery targets (CLI/cron) [#delivery-targets-clicron]
* DMs: `signal:+15551234567` (or plain E.164).
* UUID DMs: `uuid:` (or bare UUID).
* Groups: `signal:group:`.
* Usernames: `username:` (if supported by your Signal account).
Configuration reference (Signal) [#configuration-reference-signal]
Full configuration: [Configuration](/docs/gateway/configuration)
Provider options:
* `channels.signal.enabled`: enable/disable channel startup.
* `channels.signal.account`: E.164 for the bot account.
* `channels.signal.cliPath`: path to `signal-cli`.
* `channels.signal.httpUrl`: full daemon URL (overrides host/port).
* `channels.signal.httpHost`, `channels.signal.httpPort`: daemon bind (default 127.0.0.1:8080).
* `channels.signal.autoStart`: auto-spawn daemon (default true if `httpUrl` unset).
* `channels.signal.startupTimeoutMs`: startup wait timeout in ms (cap 120000).
* `channels.signal.receiveMode`: `on-start | manual`.
* `channels.signal.ignoreAttachments`: skip attachment downloads.
* `channels.signal.ignoreStories`: ignore stories from the daemon.
* `channels.signal.sendReadReceipts`: forward read receipts.
* `channels.signal.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
* `channels.signal.allowFrom`: DM allowlist (E.164 or `uuid:`). `open` requires `"*"`. Signal has no usernames; use phone/UUID ids.
* `channels.signal.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
* `channels.signal.groupAllowFrom`: group sender allowlist.
* `channels.signal.historyLimit`: max group messages to include as context (0 disables).
* `channels.signal.dmHistoryLimit`: DM history limit in user turns. Per-user overrides: `channels.signal.dms[""].historyLimit`.
* `channels.signal.textChunkLimit`: outbound chunk size (chars).
* `channels.signal.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
* `channels.signal.mediaMaxMb`: inbound/outbound media cap (MB).
Related global options:
* `agents.list[].groupChat.mentionPatterns` (Signal does not support native mentions).
* `messages.groupChat.mentionPatterns` (global fallback).
* `messages.responsePrefix`.
# Slack (/docs/channels/slack)
Slack [#slack]
Socket mode (default) [#socket-mode-default]
Quick setup (beginner) [#quick-setup-beginner]
1. Create a Slack app and enable **Socket Mode**.
2. Create an **App Token** (`xapp-...`) and **Bot Token** (`xoxb-...`).
3. Set tokens for Reeve and start the gateway.
Minimal config:
```json5
{
channels: {
slack: {
enabled: true,
appToken: "xapp-...",
botToken: "xoxb-..."
}
}
}
```
Setup [#setup]
1. Create a Slack app (From scratch) in [https://api.slack.com/apps](https://api.slack.com/apps).
2. **Socket Mode** β toggle on. Then go to **Basic Information** β **App-Level Tokens** β **Generate Token and Scopes** with scope `connections:write`. Copy the **App Token** (`xapp-...`).
3. **OAuth & Permissions** β add bot token scopes (use the manifest below). Click **Install to Workspace**. Copy the **Bot User OAuth Token** (`xoxb-...`).
4. Optional: **OAuth & Permissions** β add **User Token Scopes** (see the read-only list below). Reinstall the app and copy the **User OAuth Token** (`xoxp-...`).
5. **Event Subscriptions** β enable events and subscribe to:
* `message.*` (includes edits/deletes/thread broadcasts)
* `app_mention`
* `reaction_added`, `reaction_removed`
* `member_joined_channel`, `member_left_channel`
* `channel_rename`
* `pin_added`, `pin_removed`
6. Invite the bot to channels you want it to read.
7. Slash Commands β create `/reeve` if you use `channels.slack.slashCommand`. If you enable native commands, add one slash command per built-in command (same names as `/help`). Native defaults to off for Slack unless you set `channels.slack.commands.native: true` (global `commands.native` is `"auto"` which leaves Slack off).
8. App Home β enable the **Messages Tab** so users can DM the bot.
Use the manifest below so scopes and events stay in sync.
Multi-account support: use `channels.slack.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/docs/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
Reeve config (minimal) [#reeve-config-minimal]
Set tokens via env vars (recommended):
* `SLACK_APP_TOKEN=xapp-...`
* `SLACK_BOT_TOKEN=xoxb-...`
Or via config:
```json5
{
channels: {
slack: {
enabled: true,
appToken: "xapp-...",
botToken: "xoxb-..."
}
}
}
```
User token (optional) [#user-token-optional]
Reeve can use a Slack user token (`xoxp-...`) for read operations (history,
pins, reactions, emoji, member info). By default this stays read-only: reads
prefer the user token when present, and writes still use the bot token unless
you explicitly opt in. Even with `userTokenReadOnly: false`, the bot token stays
preferred for writes when it is available.
User tokens are configured in the config file (no env var support). For
multi-account, set `channels.slack.accounts..userToken`.
Example with bot + app + user tokens:
```json5
{
channels: {
slack: {
enabled: true,
appToken: "xapp-...",
botToken: "xoxb-...",
userToken: "xoxp-..."
}
}
}
```
Example with userTokenReadOnly explicitly set (allow user token writes):
```json5
{
channels: {
slack: {
enabled: true,
appToken: "xapp-...",
botToken: "xoxb-...",
userToken: "xoxp-...",
userTokenReadOnly: false
}
}
}
```
Token usage [#token-usage]
* Read operations (history, reactions list, pins list, emoji list, member info,
search) prefer the user token when configured, otherwise the bot token.
* Write operations (send/edit/delete messages, add/remove reactions, pin/unpin,
file uploads) use the bot token by default. If `userTokenReadOnly: false` and
no bot token is available, Reeve falls back to the user token.
History context [#history-context]
* `channels.slack.historyLimit` (or `channels.slack.accounts.*.historyLimit`) controls how many recent channel/group messages are wrapped into the prompt.
* Falls back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
HTTP mode (Events API) [#http-mode-events-api]
Use HTTP webhook mode when your Gateway is reachable by Slack over HTTPS (typical for server deployments).
HTTP mode uses the Events API + Interactivity + Slash Commands with a shared request URL.
Setup [#setup-1]
1. Create a Slack app and **disable Socket Mode** (optional if you only use HTTP).
2. **Basic Information** β copy the **Signing Secret**.
3. **OAuth & Permissions** β install the app and copy the **Bot User OAuth Token** (`xoxb-...`).
4. **Event Subscriptions** β enable events and set the **Request URL** to your gateway webhook path (default `/slack/events`).
5. **Interactivity & Shortcuts** β enable and set the same **Request URL**.
6. **Slash Commands** β set the same **Request URL** for your command(s).
Example request URL:
`https://gateway-host/slack/events`
Reeve config (minimal) [#reeve-config-minimal-1]
```json5
{
channels: {
slack: {
enabled: true,
mode: "http",
botToken: "xoxb-...",
signingSecret: "your-signing-secret",
webhookPath: "/slack/events"
}
}
}
```
Multi-account HTTP mode: set `channels.slack.accounts..mode = "http"` and provide a unique
`webhookPath` per account so each Slack app can point to its own URL.
Manifest (optional) [#manifest-optional]
Use this Slack app manifest to create the app quickly (adjust the name/command if you want). Include the
user scopes if you plan to configure a user token.
```json
{
"display_information": {
"name": "Reeve",
"description": "Slack connector for Reeve"
},
"features": {
"bot_user": {
"display_name": "Reeve",
"always_online": false
},
"app_home": {
"messages_tab_enabled": true,
"messages_tab_read_only_enabled": false
},
"slash_commands": [
{
"command": "/reeve",
"description": "Send a message to Reeve",
"should_escape": false
}
]
},
"oauth_config": {
"scopes": {
"bot": [
"chat:write",
"channels:history",
"channels:read",
"groups:history",
"groups:read",
"groups:write",
"im:history",
"im:read",
"im:write",
"mpim:history",
"mpim:read",
"mpim:write",
"users:read",
"app_mentions:read",
"reactions:read",
"reactions:write",
"pins:read",
"pins:write",
"emoji:read",
"commands",
"files:read",
"files:write"
],
"user": [
"channels:history",
"channels:read",
"groups:history",
"groups:read",
"im:history",
"im:read",
"mpim:history",
"mpim:read",
"users:read",
"reactions:read",
"pins:read",
"emoji:read",
"search:read"
]
}
},
"settings": {
"socket_mode_enabled": true,
"event_subscriptions": {
"bot_events": [
"app_mention",
"message.channels",
"message.groups",
"message.im",
"message.mpim",
"reaction_added",
"reaction_removed",
"member_joined_channel",
"member_left_channel",
"channel_rename",
"pin_added",
"pin_removed"
]
}
}
}
```
If you enable native commands, add one `slash_commands` entry per command you want to expose (matching the `/help` list). Override with `channels.slack.commands.native`.
Scopes (current vs optional) [#scopes-current-vs-optional]
Slack's Conversations API is type-scoped: you only need the scopes for the
conversation types you actually touch (channels, groups, im, mpim). See
[https://docs.slack.dev/apis/web-api/using-the-conversations-api/](https://docs.slack.dev/apis/web-api/using-the-conversations-api/) for the overview.
Bot token scopes (required) [#bot-token-scopes-required]
* `chat:write` (send/update/delete messages via `chat.postMessage`)
[https://docs.slack.dev/reference/methods/chat.postMessage](https://docs.slack.dev/reference/methods/chat.postMessage)
* `im:write` (open DMs via `conversations.open` for user DMs)
[https://docs.slack.dev/reference/methods/conversations.open](https://docs.slack.dev/reference/methods/conversations.open)
* `channels:history`, `groups:history`, `im:history`, `mpim:history`
[https://docs.slack.dev/reference/methods/conversations.history](https://docs.slack.dev/reference/methods/conversations.history)
* `channels:read`, `groups:read`, `im:read`, `mpim:read`
[https://docs.slack.dev/reference/methods/conversations.info](https://docs.slack.dev/reference/methods/conversations.info)
* `users:read` (user lookup)
[https://docs.slack.dev/reference/methods/users.info](https://docs.slack.dev/reference/methods/users.info)
* `reactions:read`, `reactions:write` (`reactions.get` / `reactions.add`)
[https://docs.slack.dev/reference/methods/reactions.get](https://docs.slack.dev/reference/methods/reactions.get)
[https://docs.slack.dev/reference/methods/reactions.add](https://docs.slack.dev/reference/methods/reactions.add)
* `pins:read`, `pins:write` (`pins.list` / `pins.add` / `pins.remove`)
[https://docs.slack.dev/reference/scopes/pins.read](https://docs.slack.dev/reference/scopes/pins.read)
[https://docs.slack.dev/reference/scopes/pins.write](https://docs.slack.dev/reference/scopes/pins.write)
* `emoji:read` (`emoji.list`)
[https://docs.slack.dev/reference/scopes/emoji.read](https://docs.slack.dev/reference/scopes/emoji.read)
* `files:write` (uploads via `files.uploadV2`)
[https://docs.slack.dev/messaging/working-with-files/#upload](https://docs.slack.dev/messaging/working-with-files/#upload)
User token scopes (optional, read-only by default) [#user-token-scopes-optional-read-only-by-default]
Add these under **User Token Scopes** if you configure `channels.slack.userToken`.
* `channels:history`, `groups:history`, `im:history`, `mpim:history`
* `channels:read`, `groups:read`, `im:read`, `mpim:read`
* `users:read`
* `reactions:read`
* `pins:read`
* `emoji:read`
* `search:read`
Not needed today (but likely future) [#not-needed-today-but-likely-future]
* `mpim:write` (only if we add group-DM open/DM start via `conversations.open`)
* `groups:write` (only if we add private-channel management: create/rename/invite/archive)
* `chat:write.public` (only if we want to post to channels the bot isn't in)
[https://docs.slack.dev/reference/scopes/chat.write.public](https://docs.slack.dev/reference/scopes/chat.write.public)
* `users:read.email` (only if we need email fields from `users.info`)
[https://docs.slack.dev/changelog/2017-04-narrowing-email-access](https://docs.slack.dev/changelog/2017-04-narrowing-email-access)
* `files:read` (only if we start listing/reading file metadata)
Config [#config]
Slack uses Socket Mode only (no HTTP webhook server). Provide both tokens:
```json
{
"slack": {
"enabled": true,
"botToken": "xoxb-...",
"appToken": "xapp-...",
"groupPolicy": "allowlist",
"dm": {
"enabled": true,
"policy": "pairing",
"allowFrom": ["U123", "U456", "*"],
"groupEnabled": false,
"groupChannels": ["G123"],
"replyToMode": "all"
},
"channels": {
"C123": { "allow": true, "requireMention": true },
"#general": {
"allow": true,
"requireMention": true,
"users": ["U123"],
"skills": ["search", "docs"],
"systemPrompt": "Keep answers short."
}
},
"reactionNotifications": "own",
"reactionAllowlist": ["U123"],
"replyToMode": "off",
"actions": {
"reactions": true,
"messages": true,
"pins": true,
"memberInfo": true,
"emojiList": true
},
"slashCommand": {
"enabled": true,
"name": "reeve",
"sessionPrefix": "slack:slash",
"ephemeral": true
},
"textChunkLimit": 4000,
"mediaMaxMb": 20
}
}
```
Tokens can also be supplied via env vars:
* `SLACK_BOT_TOKEN`
* `SLACK_APP_TOKEN`
Ack reactions are controlled globally via `messages.ackReaction` +
`messages.ackReactionScope`. Use `messages.removeAckAfterReply` to clear the
ack reaction after the bot replies.
Limits [#limits]
* Outbound text is chunked to `channels.slack.textChunkLimit` (default 4000).
* Optional newline chunking: set `channels.slack.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
* Media uploads are capped by `channels.slack.mediaMaxMb` (default 20).
Reply threading [#reply-threading]
By default, Reeve replies in the main channel. Use `channels.slack.replyToMode` to control automatic threading:
| Mode | Behavior |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `off` | **Default.** Reply in main channel. Only thread if the triggering message was already in a thread. |
| `first` | First reply goes to thread (under the triggering message), subsequent replies go to main channel. Useful for keeping context visible while avoiding thread clutter. |
| `all` | All replies go to thread. Keeps conversations contained but may reduce visibility. |
The mode applies to both auto-replies and agent tool calls (`slack sendMessage`).
Per-chat-type threading [#per-chat-type-threading]
You can configure different threading behavior per chat type by setting `channels.slack.replyToModeByChatType`:
```json5
{
channels: {
slack: {
replyToMode: "off", // default for channels
replyToModeByChatType: {
direct: "all", // DMs always thread
group: "first" // group DMs/MPIM thread first reply
},
}
}
}
```
Supported chat types:
* `direct`: 1:1 DMs (Slack `im`)
* `group`: group DMs / MPIMs (Slack `mpim`)
* `channel`: standard channels (public/private)
Precedence:
1. `replyToModeByChatType.`
2. `replyToMode`
3. Provider default (`off`)
Legacy `channels.slack.dm.replyToMode` is still accepted as a fallback for `direct` when no chat-type override is set.
Examples:
Thread DMs only:
```json5
{
channels: {
slack: {
replyToMode: "off",
replyToModeByChatType: { direct: "all" }
}
}
}
```
Thread group DMs but keep channels in the root:
```json5
{
channels: {
slack: {
replyToMode: "off",
replyToModeByChatType: { group: "first" }
}
}
}
```
Make channels thread, keep DMs in the root:
```json5
{
channels: {
slack: {
replyToMode: "first",
replyToModeByChatType: { direct: "off", group: "off" }
}
}
}
```
Manual threading tags [#manual-threading-tags]
For fine-grained control, use these tags in agent responses:
* `[[reply_to_current]]` β reply to the triggering message (start/continue thread).
* `[[reply_to:]]` β reply to a specific message id.
Sessions + routing [#sessions--routing]
* DMs share the `main` session (like WhatsApp/Telegram).
* Channels map to `agent::slack:channel:` sessions.
* Slash commands use `agent::slack:slash:` sessions (prefix configurable via `channels.slack.slashCommand.sessionPrefix`).
* If Slack doesnβt provide `channel_type`, Reeve infers it from the channel ID prefix (`D`, `C`, `G`) and defaults to `channel` to keep session keys stable.
* Native command registration uses `commands.native` (global default `"auto"` β Slack off) and can be overridden per-workspace with `channels.slack.commands.native`. Text commands require standalone `/...` messages and can be disabled with `commands.text: false`. Slack slash commands are managed in the Slack app and are not removed automatically. Use `commands.useAccessGroups: false` to bypass access-group checks for commands.
* Full command list + config: [Slash commands](/docs/tools/slash-commands)
DM security (pairing) [#dm-security-pairing]
* Default: `channels.slack.dm.policy="pairing"` β unknown DM senders get a pairing code (expires after 1 hour).
* Approve via: `reeve pairing approve slack `.
* To allow anyone: set `channels.slack.dm.policy="open"` and `channels.slack.dm.allowFrom=["*"]`.
* `channels.slack.dm.allowFrom` accepts user IDs, @handles, or emails (resolved at startup when tokens allow). The wizard accepts usernames and resolves them to ids during setup when tokens allow.
Group policy [#group-policy]
* `channels.slack.groupPolicy` controls channel handling (`open|disabled|allowlist`).
* `allowlist` requires channels to be listed in `channels.slack.channels`.
* If you only set `SLACK_BOT_TOKEN`/`SLACK_APP_TOKEN` and never create a `channels.slack` section,
the runtime defaults `groupPolicy` to `open`. Add `channels.slack.groupPolicy`,
`channels.defaults.groupPolicy`, or a channel allowlist to lock it down.
* The configure wizard accepts `#channel` names and resolves them to IDs when possible
(public + private); if multiple matches exist, it prefers the active channel.
* On startup, Reeve resolves channel/user names in allowlists to IDs (when tokens allow)
and logs the mapping; unresolved entries are kept as typed.
* To allow **no channels**, set `channels.slack.groupPolicy: "disabled"` (or keep an empty allowlist).
Channel options (`channels.slack.channels.` or `channels.slack.channels.`):
* `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
* `requireMention`: mention gating for the channel.
* `allowBots`: allow bot-authored messages in this channel (default: false).
* `users`: optional per-channel user allowlist.
* `skills`: skill filter (omit = all skills, empty = none).
* `systemPrompt`: extra system prompt for the channel (combined with topic/purpose).
* `enabled`: set `false` to disable the channel.
Delivery targets [#delivery-targets]
Use these with cron/CLI sends:
* `user:` for DMs
* `channel:` for channels
Tool actions [#tool-actions]
Slack tool actions can be gated with `channels.slack.actions.*`:
| Action group | Default | Notes |
| ------------ | ------- | ---------------------- |
| reactions | enabled | React + list reactions |
| messages | enabled | Read/send/edit/delete |
| pins | enabled | Pin/unpin/list |
| memberInfo | enabled | Member info |
| emojiList | enabled | Custom emoji list |
Security notes [#security-notes]
* Writes default to the bot token so state-changing actions stay scoped to the
app's bot permissions and identity.
* Setting `userTokenReadOnly: false` allows the user token to be used for write
operations when a bot token is unavailable, which means actions run with the
installing user's access. Treat the user token as highly privileged and keep
action gates and allowlists tight.
* If you enable user-token writes, make sure the user token includes the write
scopes you expect (`chat:write`, `reactions:write`, `pins:write`,
`files:write`) or those operations will fail.
Notes [#notes]
* Mention gating is controlled via `channels.slack.channels` (set `requireMention` to `true`); `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) also count as mentions.
* Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
* Reaction notifications follow `channels.slack.reactionNotifications` (use `reactionAllowlist` with mode `allowlist`).
* Bot-authored messages are ignored by default; enable via `channels.slack.allowBots` or `channels.slack.channels..allowBots`.
* Warning: If you allow replies to other bots (`channels.slack.allowBots=true` or `channels.slack.channels..allowBots=true`), prevent bot-to-bot reply loops with `requireMention`, `channels.slack.channels..users` allowlists, and/or clear guardrails in `AGENTS.md` and `SOUL.md`.
* For the Slack tool, reaction removal semantics are in [/tools/reactions](/docs/tools/reactions).
* Attachments are downloaded to the media store when permitted and under the size limit.
# Telegram (Bot API) (/docs/channels/telegram)
Telegram (Bot API) [#telegram-bot-api]
Status: production-ready for bot DMs + groups via grammY. Long-polling by default; webhook optional.
Quick setup (beginner) [#quick-setup-beginner]
1. Create a bot with **@BotFather** and copy the token.
2. Set the token:
* Env: `TELEGRAM_BOT_TOKEN=...`
* Or config: `channels.telegram.botToken: "..."`.
* If both are set, config takes precedence (env fallback is default-account only).
3. Start the gateway.
4. DM access is pairing by default; approve the pairing code on first contact.
Minimal config:
```json5
{
channels: {
telegram: {
enabled: true,
botToken: "123:abc",
dmPolicy: "pairing"
}
}
}
```
What it is [#what-it-is]
* A Telegram Bot API channel owned by the Gateway.
* Deterministic routing: replies go back to Telegram; the model never chooses channels.
* DMs share the agent's main session; groups stay isolated (`agent::telegram:group:`).
Setup (fast path) [#setup-fast-path]
1) Create a bot token (BotFather) [#1-create-a-bot-token-botfather]
1. Open Telegram and chat with **@BotFather**.
2. Run `/newbot`, then follow the prompts (name + username ending in `bot`).
3. Copy the token and store it safely.
Optional BotFather settings:
* `/setjoingroups` β allow/deny adding the bot to groups.
* `/setprivacy` β control whether the bot sees all group messages.
2) Configure the token (env or config) [#2-configure-the-token-env-or-config]
Example:
```json5
{
channels: {
telegram: {
enabled: true,
botToken: "123:abc",
dmPolicy: "pairing",
groups: { "*": { requireMention: true } }
}
}
}
```
Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account).
If both env and config are set, config takes precedence.
Multi-account support: use `channels.telegram.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/docs/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern.
3. Start the gateway. Telegram starts when a token is resolved (config first, env fallback).
4. DM access defaults to pairing. Approve the code when the bot is first contacted.
5. For groups: add the bot, decide privacy/admin behavior (below), then set `channels.telegram.groups` to control mention gating + allowlists.
Token + privacy + permissions (Telegram side) [#token--privacy--permissions-telegram-side]
Token creation (BotFather) [#token-creation-botfather]
* `/newbot` creates the bot and returns the token (keep it secret).
* If a token leaks, revoke/regenerate it via @BotFather and update your config.
Group message visibility (Privacy Mode) [#group-message-visibility-privacy-mode]
Telegram bots default to **Privacy Mode**, which limits which group messages they receive.
If your bot must see *all* group messages, you have two options:
* Disable privacy mode with `/setprivacy` **or**
* Add the bot as a group **admin** (admin bots receive all messages).
**Note:** When you toggle privacy mode, Telegram requires removing + reβadding the bot
to each group for the change to take effect.
Group permissions (admin rights) [#group-permissions-admin-rights]
Admin status is set inside the group (Telegram UI). Admin bots always receive all
group messages, so use admin if you need full visibility.
How it works (behavior) [#how-it-works-behavior]
* Inbound messages are normalized into the shared channel envelope with reply context and media placeholders.
* Group replies require a mention by default (native @mention or `agents.list[].groupChat.mentionPatterns` / `messages.groupChat.mentionPatterns`).
* Multi-agent override: set per-agent patterns on `agents.list[].groupChat.mentionPatterns`.
* Replies always route back to the same Telegram chat.
* Long-polling uses grammY runner with per-chat sequencing; overall concurrency is capped by `agents.defaults.maxConcurrent`.
* Telegram Bot API does not support read receipts; there is no `sendReadReceipts` option.
Formatting (Telegram HTML) [#formatting-telegram-html]
* Outbound Telegram text uses `parse_mode: "HTML"` (Telegramβs supported tag subset).
* Markdown-ish input is rendered into **Telegram-safe HTML** (bold/italic/strike/code/links); block elements are flattened to text with newlines/bullets.
* Raw HTML from models is escaped to avoid Telegram parse errors.
* If Telegram rejects the HTML payload, Reeve retries the same message as plain text.
Commands (native + custom) [#commands-native--custom]
Reeve registers native commands (like `/status`, `/reset`, `/model`) with Telegramβs bot menu on startup.
You can add custom commands to the menu via config:
```json5
{
channels: {
telegram: {
customCommands: [
{ command: "backup", description: "Git backup" },
{ command: "generate", description: "Create an image" }
]
}
}
}
```
Troubleshooting [#troubleshooting]
* `setMyCommands failed` in logs usually means outbound HTTPS/DNS is blocked to `api.telegram.org`.
* If you see `sendMessage` or `sendChatAction` failures, check IPv6 routing and DNS.
More help: [Channel troubleshooting](/docs/channels/troubleshooting).
Notes:
* Custom commands are **menu entries only**; Reeve does not implement them unless you handle them elsewhere.
* Command names are normalized (leading `/` stripped, lowercased) and must match `a-z`, `0-9`, `_` (1β32 chars).
* Custom commands **cannot override native commands**. Conflicts are ignored and logged.
* If `commands.native` is disabled, only custom commands are registered (or cleared if none).
Limits [#limits]
* Outbound text is chunked to `channels.telegram.textChunkLimit` (default 4000).
* Optional newline chunking: set `channels.telegram.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
* Media downloads/uploads are capped by `channels.telegram.mediaMaxMb` (default 5).
* Telegram Bot API requests time out after `channels.telegram.timeoutSeconds` (default 500 via grammY). Set lower to avoid long hangs.
* Group history context uses `channels.telegram.historyLimit` (or `channels.telegram.accounts.*.historyLimit`), falling back to `messages.groupChat.historyLimit`. Set `0` to disable (default 50).
* DM history can be limited with `channels.telegram.dmHistoryLimit` (user turns). Per-user overrides: `channels.telegram.dms[""].historyLimit`.
Group activation modes [#group-activation-modes]
By default, the bot only responds to mentions in groups (`@botname` or patterns in `agents.list[].groupChat.mentionPatterns`). To change this behavior:
Via config (recommended) [#via-config-recommended]
```json5
{
channels: {
telegram: {
groups: {
"-1001234567890": { requireMention: false } // always respond in this group
}
}
}
}
```
**Important:** Setting `channels.telegram.groups` creates an **allowlist** - only listed groups (or `"*"`) will be accepted.
Forum topics inherit their parent group config (allowFrom, requireMention, skills, prompts) unless you add per-topic overrides under `channels.telegram.groups..topics.`.
To allow all groups with always-respond:
```json5
{
channels: {
telegram: {
groups: {
"*": { requireMention: false } // all groups, always respond
}
}
}
}
```
To keep mention-only for all groups (default behavior):
```json5
{
channels: {
telegram: {
groups: {
"*": { requireMention: true } // or omit groups entirely
}
}
}
}
```
Via command (session-level) [#via-command-session-level]
Send in the group:
* `/activation always` - respond to all messages
* `/activation mention` - require mentions (default)
**Note:** Commands update session state only. For persistent behavior across restarts, use config.
Getting the group chat ID [#getting-the-group-chat-id]
Forward any message from the group to `@userinfobot` or `@getidsbot` on Telegram to see the chat ID (negative number like `-1001234567890`).
**Tip:** For your own user ID, DM the bot and it will reply with your user ID (pairing message), or use `/whoami` once commands are enabled.
**Privacy note:** `@userinfobot` is a third-party bot. If you prefer, add the bot to the group, send a message, and use `reeve logs --follow` to read `chat.id`, or use the Bot API `getUpdates`.
Config writes [#config-writes]
By default, Telegram is allowed to write config updates triggered by channel events or `/config set|unset`.
This happens when:
* A group is upgraded to a supergroup and Telegram emits `migrate_to_chat_id` (chat ID changes). Reeve can migrate `channels.telegram.groups` automatically.
* You run `/config set` or `/config unset` in a Telegram chat (requires `commands.config: true`).
Disable with:
```json5
{
channels: { telegram: { configWrites: false } }
}
```
Topics (forum supergroups) [#topics-forum-supergroups]
Telegram forum topics include a `message_thread_id` per message. Reeve:
* Appends `:topic:` to the Telegram group session key so each topic is isolated.
* Sends typing indicators and replies with `message_thread_id` so responses stay in the topic.
* General topic (thread id `1`) is special: message sends omit `message_thread_id` (Telegram rejects it), but typing indicators still include it.
* Exposes `MessageThreadId` + `IsForum` in template context for routing/templating.
* Topic-specific configuration is available under `channels.telegram.groups..topics.` (skills, allowlists, auto-reply, system prompts, disable).
* Topic configs inherit group settings (requireMention, allowlists, skills, prompts, enabled) unless overridden per topic.
Private chats can include `message_thread_id` in some edge cases. Reeve keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present.
Inline Buttons [#inline-buttons]
Telegram supports inline keyboards with callback buttons.
```json5
{
"channels": {
"telegram": {
"capabilities": {
"inlineButtons": "allowlist"
}
}
}
}
```
For per-account configuration:
```json5
{
"channels": {
"telegram": {
"accounts": {
"main": {
"capabilities": {
"inlineButtons": "allowlist"
}
}
}
}
}
}
```
Scopes:
* `off` β inline buttons disabled
* `dm` β only DMs (group targets blocked)
* `group` β only groups (DM targets blocked)
* `all` β DMs + groups
* `allowlist` β DMs + groups, but only senders allowed by `allowFrom`/`groupAllowFrom` (same rules as control commands)
Default: `allowlist`.
Legacy: `capabilities: ["inlineButtons"]` = `inlineButtons: "all"`.
Sending buttons [#sending-buttons]
Use the message tool with the `buttons` parameter:
```json5
{
"action": "send",
"channel": "telegram",
"to": "123456789",
"message": "Choose an option:",
"buttons": [
[
{"text": "Yes", "callback_data": "yes"},
{"text": "No", "callback_data": "no"}
],
[
{"text": "Cancel", "callback_data": "cancel"}
]
]
}
```
When a user clicks a button, the callback data is sent back to the agent as a message with the format:
`callback_data: value`
Configuration options [#configuration-options]
Telegram capabilities can be configured at two levels (object form shown above; legacy string arrays still supported):
* `channels.telegram.capabilities`: Global default capability config applied to all Telegram accounts unless overridden.
* `channels.telegram.accounts..capabilities`: Per-account capabilities that override the global defaults for that specific account.
Use the global setting when all Telegram bots/accounts should behave the same. Use per-account configuration when different bots need different behaviors (for example, one account only handles DMs while another is allowed in groups).
Access control (DMs + groups) [#access-control-dms--groups]
DM access [#dm-access]
* Default: `channels.telegram.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
* Approve via:
* `reeve pairing list telegram`
* `reeve pairing approve telegram `
* Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/docs/start/pairing)
* `channels.telegram.allowFrom` accepts numeric user IDs (recommended) or `@username` entries. It is **not** the bot username; use the human senderβs ID. The wizard accepts `@username` and resolves it to the numeric ID when possible.
Finding your Telegram user ID [#finding-your-telegram-user-id]
Safer (no third-party bot):
1. Start the gateway and DM your bot.
2. Run `reeve logs --follow` and look for `from.id`.
Alternate (official Bot API):
1. DM your bot.
2. Fetch updates with your bot token and read `message.from.id`:
```bash
curl "https://api.telegram.org/bot/getUpdates"
```
Third-party (less private):
* DM `@userinfobot` or `@getidsbot` and use the returned user id.
Group access [#group-access]
Two independent controls:
**1. Which groups are allowed** (group allowlist via `channels.telegram.groups`):
* No `groups` config = all groups allowed
* With `groups` config = only listed groups or `"*"` are allowed
* Example: `"groups": { "-1001234567890": {}, "*": {} }` allows all groups
**2. Which senders are allowed** (sender filtering via `channels.telegram.groupPolicy`):
* `"open"` = all senders in allowed groups can message
* `"allowlist"` = only senders in `channels.telegram.groupAllowFrom` can message
* `"disabled"` = no group messages accepted at all
Default is `groupPolicy: "allowlist"` (blocked unless you add `groupAllowFrom`).
Most users want: `groupPolicy: "allowlist"` + `groupAllowFrom` + specific groups listed in `channels.telegram.groups`
Long-polling vs webhook [#long-polling-vs-webhook]
* Default: long-polling (no public URL required).
* Webhook mode: set `channels.telegram.webhookUrl` (optionally `channels.telegram.webhookSecret` + `channels.telegram.webhookPath`).
* The local listener binds to `0.0.0.0:8787` and serves `POST /telegram-webhook` by default.
* If your public URL is different, use a reverse proxy and point `channels.telegram.webhookUrl` at the public endpoint.
Reply threading [#reply-threading]
Telegram supports optional threaded replies via tags:
* `[[reply_to_current]]` -- reply to the triggering message.
* `[[reply_to:]]` -- reply to a specific message id.
Controlled by `channels.telegram.replyToMode`:
* `first` (default), `all`, `off`.
Audio messages (voice vs file) [#audio-messages-voice-vs-file]
Telegram distinguishes **voice notes** (round bubble) from **audio files** (metadata card).
Reeve defaults to audio files for backward compatibility.
To force a voice note bubble in agent replies, include this tag anywhere in the reply:
* `[[audio_as_voice]]` β send audio as a voice note instead of a file.
The tag is stripped from the delivered text. Other channels ignore this tag.
For message tool sends, set `asVoice: true` with a voice-compatible audio `media` URL
(`message` is optional when media is present):
```json5
{
"action": "send",
"channel": "telegram",
"to": "123456789",
"media": "https://example.com/voice.ogg",
"asVoice": true
}
```
Streaming (drafts) [#streaming-drafts]
Telegram can stream **draft bubbles** while the agent is generating a response.
Reeve uses Bot API `sendMessageDraft` (not real messages) and then sends the
final reply as a normal message.
Requirements (Telegram Bot API 9.3+):
* **Private chats with topics enabled** (forum topic mode for the bot).
* Incoming messages must include `message_thread_id` (private topic thread).
* Streaming is ignored for groups/supergroups/channels.
Config:
* `channels.telegram.streamMode: "off" | "partial" | "block"` (default: `partial`)
* `partial`: update the draft bubble with the latest streaming text.
* `block`: update the draft bubble in larger blocks (chunked).
* `off`: disable draft streaming.
* Optional (only for `streamMode: "block"`):
* `channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }`
* defaults: `minChars: 200`, `maxChars: 800`, `breakPreference: "paragraph"` (clamped to `channels.telegram.textChunkLimit`).
Note: draft streaming is separate from **block streaming** (channel messages).
Block streaming is off by default and requires `channels.telegram.blockStreaming: true`
if you want early Telegram messages instead of draft updates.
Reasoning stream (Telegram only):
* `/reasoning stream` streams reasoning into the draft bubble while the reply is
generating, then sends the final answer without reasoning.
* If `channels.telegram.streamMode` is `off`, reasoning stream is disabled.
More context: [Streaming + chunking](/docs/concepts/streaming).
Retry policy [#retry-policy]
Outbound Telegram API calls retry on transient network/429 errors with exponential backoff and jitter. Configure via `channels.telegram.retry`. See [Retry policy](/docs/concepts/retry).
Agent tool (messages + reactions) [#agent-tool-messages--reactions]
* Tool: `telegram` with `sendMessage` action (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`).
* Tool: `telegram` with `react` action (`chatId`, `messageId`, `emoji`).
* Tool: `telegram` with `deleteMessage` action (`chatId`, `messageId`).
* Reaction removal semantics: see [/tools/reactions](/docs/tools/reactions).
* Tool gating: `channels.telegram.actions.reactions`, `channels.telegram.actions.sendMessage`, `channels.telegram.actions.deleteMessage` (default: enabled).
Reaction notifications [#reaction-notifications]
**How reactions work:**
Telegram reactions arrive as **separate `message_reaction` events**, not as properties in message payloads. When a user adds a reaction, Reeve:
1. Receives the `message_reaction` update from Telegram API
2. Converts it to a **system event** with format: `"Telegram reaction added: {emoji} by {user} on msg {id}"`
3. Enqueues the system event using the **same session key** as regular messages
4. When the next message arrives in that conversation, system events are drained and prepended to the agent's context
The agent sees reactions as **system notifications** in the conversation history, not as message metadata.
**Configuration:**
* `channels.telegram.reactionNotifications`: Controls which reactions trigger notifications
* `"off"` β ignore all reactions
* `"own"` β notify when users react to bot messages (best-effort; in-memory) (default)
* `"all"` β notify for all reactions
* `channels.telegram.reactionLevel`: Controls agent's reaction capability
* `"off"` β agent cannot react to messages
* `"ack"` β bot sends acknowledgment reactions (π while processing) (default)
* `"minimal"` β agent can react sparingly (guideline: 1 per 5-10 exchanges)
* `"extensive"` β agent can react liberally when appropriate
**Forum groups:** Reactions in forum groups include `message_thread_id` and use session keys like `agent:main:telegram:group:{chatId}:topic:{threadId}`. This ensures reactions and messages in the same topic stay together.
**Example config:**
```json5
{
channels: {
telegram: {
reactionNotifications: "all", // See all reactions
reactionLevel: "minimal" // Agent can react sparingly
}
}
}
```
**Requirements:**
* Telegram bots must explicitly request `message_reaction` in `allowed_updates` (configured automatically by Reeve)
* For webhook mode, reactions are included in the webhook `allowed_updates`
* For polling mode, reactions are included in the `getUpdates` `allowed_updates`
Delivery targets (CLI/cron) [#delivery-targets-clicron]
* Use a chat id (`123456789`) or a username (`@name`) as the target.
* Example: `reeve message send --channel telegram --target 123456789 --message "hi"`.
Troubleshooting [#troubleshooting-1]
**Bot doesnβt respond to non-mention messages in a group:**
* If you set `channels.telegram.groups.*.requireMention=false`, Telegramβs Bot API **privacy mode** must be disabled.
* BotFather: `/setprivacy` β **Disable** (then remove + re-add the bot to the group)
* `reeve channels status` shows a warning when config expects unmentioned group messages.
* `reeve channels status --probe` can additionally check membership for explicit numeric group IDs (it canβt audit wildcard `"*"` rules).
* Quick test: `/activation always` (session-only; use config for persistence)
**Bot not seeing group messages at all:**
* If `channels.telegram.groups` is set, the group must be listed or use `"*"`
* Check Privacy Settings in @BotFather β "Group Privacy" should be **OFF**
* Verify bot is actually a member (not just an admin with no read access)
* Check gateway logs: `reeve logs --follow` (look for "skipping group message")
**Bot responds to mentions but not `/activation always`:**
* The `/activation` command updates session state but doesn't persist to config
* For persistent behavior, add group to `channels.telegram.groups` with `requireMention: false`
**Commands like `/status` don't work:**
* Make sure your Telegram user ID is authorized (via pairing or `channels.telegram.allowFrom`)
* Commands require authorization even in groups with `groupPolicy: "open"`
**Long-polling aborts immediately on Node 22+ (often with proxies/custom fetch):**
* Node 22+ is stricter about `AbortSignal` instances; foreign signals can abort `fetch` calls right away.
* Upgrade to a Reeve build that normalizes abort signals, or run the gateway on Node 20 until you can upgrade.
**Bot starts, then silently stops responding (or logs `HttpError: Network request ... failed`):**
* Some hosts resolve `api.telegram.org` to IPv6 first. If your server does not have working IPv6 egress, grammY can get stuck on IPv6-only requests.
* Fix by enabling IPv6 egress **or** forcing IPv4 resolution for `api.telegram.org` (for example, add an `/etc/hosts` entry using the IPv4 A record, or prefer IPv4 in your OS DNS stack), then restart the gateway.
* Quick check: `dig +short api.telegram.org A` and `dig +short api.telegram.org AAAA` to confirm what DNS returns.
Configuration reference (Telegram) [#configuration-reference-telegram]
Full configuration: [Configuration](/docs/gateway/configuration)
Provider options:
* `channels.telegram.enabled`: enable/disable channel startup.
* `channels.telegram.botToken`: bot token (BotFather).
* `channels.telegram.tokenFile`: read token from file path.
* `channels.telegram.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
* `channels.telegram.allowFrom`: DM allowlist (ids/usernames). `open` requires `"*"`.
* `channels.telegram.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
* `channels.telegram.groupAllowFrom`: group sender allowlist (ids/usernames).
* `channels.telegram.groups`: per-group defaults + allowlist (use `"*"` for global defaults).
* `channels.telegram.groups..requireMention`: mention gating default.
* `channels.telegram.groups..skills`: skill filter (omit = all skills, empty = none).
* `channels.telegram.groups..allowFrom`: per-group sender allowlist override.
* `channels.telegram.groups..systemPrompt`: extra system prompt for the group.
* `channels.telegram.groups..enabled`: disable the group when `false`.
* `channels.telegram.groups..topics..*`: per-topic overrides (same fields as group).
* `channels.telegram.groups..topics..requireMention`: per-topic mention gating override.
* `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist).
* `channels.telegram.accounts..capabilities.inlineButtons`: per-account override.
* `channels.telegram.replyToMode`: `off | first | all` (default: `first`).
* `channels.telegram.textChunkLimit`: outbound chunk size (chars).
* `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
* `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true).
* `channels.telegram.streamMode`: `off | partial | block` (draft streaming).
* `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
* `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
* `channels.telegram.proxy`: proxy URL for Bot API calls (SOCKS/HTTP).
* `channels.telegram.webhookUrl`: enable webhook mode.
* `channels.telegram.webhookSecret`: webhook secret (optional).
* `channels.telegram.webhookPath`: local webhook path (default `/telegram-webhook`).
* `channels.telegram.actions.reactions`: gate Telegram tool reactions.
* `channels.telegram.actions.sendMessage`: gate Telegram tool message sends.
* `channels.telegram.actions.deleteMessage`: gate Telegram tool message deletes.
* `channels.telegram.reactionNotifications`: `off | own | all` β control which reactions trigger system events (default: `own` when not set).
* `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` β control agent's reaction capability (default: `minimal` when not set).
Related global options:
* `agents.list[].groupChat.mentionPatterns` (mention gating patterns).
* `messages.groupChat.mentionPatterns` (global fallback).
* `commands.native` (defaults to `"auto"` β on for Telegram/Discord, off for Slack), `commands.text`, `commands.useAccessGroups` (command behavior). Override with `channels.telegram.commands.native`.
* `messages.responsePrefix`, `messages.ackReaction`, `messages.ackReactionScope`, `messages.removeAckAfterReply`.
# Tlon (plugin) (/docs/channels/tlon)
Tlon (plugin) [#tlon-plugin]
Tlon is a decentralized messenger built on Urbit. Reeve connects to your Urbit ship and can
respond to DMs and group chat messages. Group replies require an @ mention by default and can
be further restricted via allowlists.
Status: supported via plugin. DMs, group mentions, thread replies, and text-only media fallback
(URL appended to caption). Reactions, polls, and native media uploads are not supported.
Plugin required [#plugin-required]
Tlon ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
```bash
reeve plugins install @reeve/tlon
```
Local checkout (when running from a git repo):
```bash
reeve plugins install ./extensions/tlon
```
Details: [Plugins](/plugin)
Setup [#setup]
1. Install the Tlon plugin.
2. Gather your ship URL and login code.
3. Configure `channels.tlon`.
4. Restart the gateway.
5. DM the bot or mention it in a group channel.
Minimal config (single account):
```json5
{
channels: {
tlon: {
enabled: true,
ship: "~sampel-palnet",
url: "https://your-ship-host",
code: "lidlut-tabwed-pillex-ridrup"
}
}
}
```
Group channels [#group-channels]
Auto-discovery is enabled by default. You can also pin channels manually:
```json5
{
channels: {
tlon: {
groupChannels: [
"chat/~host-ship/general",
"chat/~host-ship/support"
]
}
}
}
```
Disable auto-discovery:
```json5
{
channels: {
tlon: {
autoDiscoverChannels: false
}
}
}
```
Access control [#access-control]
DM allowlist (empty = allow all):
```json5
{
channels: {
tlon: {
dmAllowlist: ["~zod", "~nec"]
}
}
}
```
Group authorization (restricted by default):
```json5
{
channels: {
tlon: {
defaultAuthorizedShips: ["~zod"],
authorization: {
channelRules: {
"chat/~host-ship/general": {
mode: "restricted",
allowedShips: ["~zod", "~nec"]
},
"chat/~host-ship/announcements": {
mode: "open"
}
}
}
}
}
}
```
Delivery targets (CLI/cron) [#delivery-targets-clicron]
Use these with `reeve message send` or cron delivery:
* DM: `~sampel-palnet` or `dm/~sampel-palnet`
* Group: `chat/~host-ship/channel` or `group:~host-ship/channel`
Notes [#notes]
* Group replies require a mention (e.g. `~your-bot-ship`) to respond.
* Thread replies: if the inbound message is in a thread, Reeve replies in-thread.
* Media: `sendMedia` falls back to text + URL (no native upload).
# Channel troubleshooting (/docs/channels/troubleshooting)
Channel troubleshooting [#channel-troubleshooting]
Start with:
```bash
reeve doctor
reeve channels status --probe
```
`channels status --probe` prints warnings when it can detect common channel misconfigurations, and includes small live checks (credentials, some permissions/membership).
Channels [#channels]
* Discord: [/channels/discord#troubleshooting](/docs/channels/discord#troubleshooting)
* Telegram: [/channels/telegram#troubleshooting](/docs/channels/telegram#troubleshooting)
* WhatsApp: [/channels/whatsapp#troubleshooting-quick](/docs/channels/whatsapp#troubleshooting-quick)
Telegram quick fixes [#telegram-quick-fixes]
* Logs show `HttpError: Network request for 'sendMessage' failed` or `sendChatAction` β check IPv6 DNS. If `api.telegram.org` resolves to IPv6 first and the host lacks IPv6 egress, force IPv4 or enable IPv6. See [/channels/telegram#troubleshooting](/docs/channels/telegram#troubleshooting).
* Logs show `setMyCommands failed` β check outbound HTTPS and DNS reachability to `api.telegram.org` (common on locked-down VPS or proxies).
# WhatsApp (web channel) (/docs/channels/whatsapp)
WhatsApp (web channel) [#whatsapp-web-channel]
Status: WhatsApp Web via Baileys only. Gateway owns the session(s).
Quick setup (beginner) [#quick-setup-beginner]
1. Use a **separate phone number** if possible (recommended).
2. Configure WhatsApp in `~/.reeve/reeve.json`.
3. Run `reeve channels login` to scan the QR code (Linked Devices).
4. Start the gateway.
Minimal config:
```json5
{
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551234567"]
}
}
}
```
Goals [#goals]
* Multiple WhatsApp accounts (multi-account) in one Gateway process.
* Deterministic routing: replies return to WhatsApp, no model routing.
* Model sees enough context to understand quoted replies.
Config writes [#config-writes]
By default, WhatsApp is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
Disable with:
```json5
{
channels: { whatsapp: { configWrites: false } }
}
```
Architecture (who owns what) [#architecture-who-owns-what]
* **Gateway** owns the Baileys socket and inbox loop.
* **CLI / macOS app** talk to the gateway; no direct Baileys use.
* **Active listener** is required for outbound sends; otherwise send fails fast.
Getting a phone number (two modes) [#getting-a-phone-number-two-modes]
WhatsApp requires a real mobile number for verification. VoIP and virtual numbers are usually blocked. There are two supported ways to run Reeve on WhatsApp:
Dedicated number (recommended) [#dedicated-number-recommended]
Use a **separate phone number** for Reeve. Best UX, clean routing, no self-chat quirks. Ideal setup: **spare/old Android phone + eSIM**. Leave it on WiβFi and power, and link it via QR.
**WhatsApp Business:** You can use WhatsApp Business on the same device with a different number. Great for keeping your personal WhatsApp separate β install WhatsApp Business and register the Reeve number there.
**Sample config (dedicated number, single-user allowlist):**
```json5
{
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551234567"]
}
}
}
```
**Pairing mode (optional):**
If you want pairing instead of allowlist, set `channels.whatsapp.dmPolicy` to `pairing`. Unknown senders get a pairing code; approve with:
`reeve pairing approve whatsapp `
Personal number (fallback) [#personal-number-fallback]
Quick fallback: run Reeve on **your own number**. Message yourself (WhatsApp βMessage yourselfβ) for testing so you donβt spam contacts. Expect to read verification codes on your main phone during setup and experiments. **Must enable self-chat mode.**
When the wizard asks for your personal WhatsApp number, enter the phone you will message from (the owner/sender), not the assistant number.
**Sample config (personal number, self-chat):**
```json
{
"whatsapp": {
"selfChatMode": true,
"dmPolicy": "allowlist",
"allowFrom": ["+15551234567"]
}
}
```
Self-chat replies default to `[{identity.name}]` when set (otherwise `[reeve]`)
if `messages.responsePrefix` is unset. Set it explicitly to customize or disable
the prefix (use `""` to remove it).
Number sourcing tips [#number-sourcing-tips]
* **Local eSIM** from your country's mobile carrier (most reliable)
* Austria: [hot.at](https://www.hot.at)
* UK: [giffgaff](https://www.giffgaff.com) β free SIM, no contract
* **Prepaid SIM** β cheap, just needs to receive one SMS for verification
**Avoid:** TextNow, Google Voice, most "free SMS" services β WhatsApp blocks these aggressively.
**Tip:** The number only needs to receive one verification SMS. After that, WhatsApp Web sessions persist via `creds.json`.
Why Not Twilio? [#why-not-twilio]
* Early Reeve builds supported Twilioβs WhatsApp Business integration.
* WhatsApp Business numbers are a poor fit for a personal assistant.
* Meta enforces a 24βhour reply window; if you havenβt responded in the last 24 hours, the business number canβt initiate new messages.
* High-volume or βchattyβ usage triggers aggressive blocking, because business accounts arenβt meant to send dozens of personal assistant messages.
* Result: unreliable delivery and frequent blocks, so support was removed.
Login + credentials [#login--credentials]
* Login command: `reeve channels login` (QR via Linked Devices).
* Multi-account login: `reeve channels login --account ` (`` = `accountId`).
* Default account (when `--account` is omitted): `default` if present, otherwise the first configured account id (sorted).
* Credentials stored in `~/.reeve/credentials/whatsapp//creds.json`.
* Backup copy at `creds.json.bak` (restored on corruption).
* Legacy compatibility: older installs stored Baileys files directly in `~/.reeve/credentials/`.
* Logout: `reeve channels logout` (or `--account `) deletes WhatsApp auth state (but keeps shared `oauth.json`).
* Logged-out socket => error instructs re-link.
Inbound flow (DM + group) [#inbound-flow-dm--group]
* WhatsApp events come from `messages.upsert` (Baileys).
* Inbox listeners are detached on shutdown to avoid accumulating event handlers in tests/restarts.
* Status/broadcast chats are ignored.
* Direct chats use E.164; groups use group JID.
* **DM policy**: `channels.whatsapp.dmPolicy` controls direct chat access (default: `pairing`).
* Pairing: unknown senders get a pairing code (approve via `reeve pairing approve whatsapp `; codes expire after 1 hour).
* Open: requires `channels.whatsapp.allowFrom` to include `"*"`.
* Self messages are always allowed; βself-chat modeβ still requires `channels.whatsapp.allowFrom` to include your own number.
Personal-number mode (fallback) [#personal-number-mode-fallback]
If you run Reeve on your **personal WhatsApp number**, enable `channels.whatsapp.selfChatMode` (see sample above).
Behavior:
* Outbound DMs never trigger pairing replies (prevents spamming contacts).
* Inbound unknown senders still follow `channels.whatsapp.dmPolicy`.
* Self-chat mode (allowFrom includes your number) avoids auto read receipts and ignores mention JIDs.
* Read receipts sent for non-self-chat DMs.
Read receipts [#read-receipts]
By default, the gateway marks inbound WhatsApp messages as read (blue ticks) once they are accepted.
Disable globally:
```json5
{
channels: { whatsapp: { sendReadReceipts: false } }
}
```
Disable per account:
```json5
{
channels: {
whatsapp: {
accounts: {
personal: { sendReadReceipts: false }
}
}
}
}
```
Notes:
* Self-chat mode always skips read receipts.
WhatsApp FAQ: sending messages + pairing [#whatsapp-faq-sending-messages--pairing]
**Will Reeve message random contacts when I link WhatsApp?**\
No. Default DM policy is **pairing**, so unknown senders only get a pairing code and their message is **not processed**. Reeve only replies to chats it receives, or to sends you explicitly trigger (agent/CLI).
**How does pairing work on WhatsApp?**\
Pairing is a DM gate for unknown senders:
* First DM from a new sender returns a short code (message is not processed).
* Approve with: `reeve pairing approve whatsapp ` (list with `reeve pairing list whatsapp`).
* Codes expire after 1 hour; pending requests are capped at 3 per channel.
**Can multiple people use different Reeves on one WhatsApp number?**\
Yes, by routing each sender to a different agent via `bindings` (peer `kind: "dm"`, sender E.164 like `+15551234567`). Replies still come from the **same WhatsApp account**, and direct chats collapse to each agentβs main session, so use **one agent per person**. DM access control (`dmPolicy`/`allowFrom`) is global per WhatsApp account. See [Multi-Agent Routing](/docs/concepts/multi-agent).
**Why do you ask for my phone number in the wizard?**\
The wizard uses it to set your **allowlist/owner** so your own DMs are permitted. Itβs not used for auto-sending. If you run on your personal WhatsApp number, use that same number and enable `channels.whatsapp.selfChatMode`.
Message normalization (what the model sees) [#message-normalization-what-the-model-sees]
* `Body` is the current message body with envelope.
* Quoted reply context is **always appended**:
```
[Replying to +1555 id:ABC123]
>
[/Replying]
```
* Reply metadata also set:
* `ReplyToId` = stanzaId
* `ReplyToBody` = quoted body or media placeholder
* `ReplyToSender` = E.164 when known
* Media-only inbound messages use placeholders:
* ``
Groups [#groups]
* Groups map to `agent::whatsapp:group:` sessions.
* Group policy: `channels.whatsapp.groupPolicy = open|disabled|allowlist` (default `allowlist`).
* Activation modes:
* `mention` (default): requires @mention or regex match.
* `always`: always triggers.
* `/activation mention|always` is owner-only and must be sent as a standalone message.
* Owner = `channels.whatsapp.allowFrom` (or self E.164 if unset).
* **History injection** (pending-only):
* Recent *unprocessed* messages (default 50) inserted under:
`[Chat messages since your last reply - for context]` (messages already in the session are not re-injected)
* Current message under:
`[Current message - respond to this]`
* Sender suffix appended: `[from: Name (+E164)]`
* Group metadata cached 5 min (subject + participants).
Reply delivery (threading) [#reply-delivery-threading]
* WhatsApp Web sends standard messages (no quoted reply threading in the current gateway).
* Reply tags are ignored on this channel.
Acknowledgment reactions (auto-react on receipt) [#acknowledgment-reactions-auto-react-on-receipt]
WhatsApp can automatically send emoji reactions to incoming messages immediately upon receipt, before the bot generates a reply. This provides instant feedback to users that their message was received.
**Configuration:**
```json
{
"whatsapp": {
"ackReaction": {
"emoji": "π",
"direct": true,
"group": "mentions"
}
}
}
```
**Options:**
* `emoji` (string): Emoji to use for acknowledgment (e.g., "π", "β ", "π¨"). Empty or omitted = feature disabled.
* `direct` (boolean, default: `true`): Send reactions in direct/DM chats.
* `group` (string, default: `"mentions"`): Group chat behavior:
* `"always"`: React to all group messages (even without @mention)
* `"mentions"`: React only when bot is @mentioned
* `"never"`: Never react in groups
**Per-account override:**
```json
{
"whatsapp": {
"accounts": {
"work": {
"ackReaction": {
"emoji": "β ",
"direct": false,
"group": "always"
}
}
}
}
}
```
**Behavior notes:**
* Reactions are sent **immediately** upon message receipt, before typing indicators or bot replies.
* In groups with `requireMention: false` (activation: always), `group: "mentions"` will react to all messages (not just @mentions).
* Fire-and-forget: reaction failures are logged but don't prevent the bot from replying.
* Participant JID is automatically included for group reactions.
* WhatsApp ignores `messages.ackReaction`; use `channels.whatsapp.ackReaction` instead.
Agent tool (reactions) [#agent-tool-reactions]
* Tool: `whatsapp` with `react` action (`chatJid`, `messageId`, `emoji`, optional `remove`).
* Optional: `participant` (group sender), `fromMe` (reacting to your own message), `accountId` (multi-account).
* Reaction removal semantics: see [/tools/reactions](/docs/tools/reactions).
* Tool gating: `channels.whatsapp.actions.reactions` (default: enabled).
Limits [#limits]
* Outbound text is chunked to `channels.whatsapp.textChunkLimit` (default 4000).
* Optional newline chunking: set `channels.whatsapp.chunkMode="newline"` to split on blank lines (paragraph boundaries) before length chunking.
* Inbound media saves are capped by `channels.whatsapp.mediaMaxMb` (default 50 MB).
* Outbound media items are capped by `agents.defaults.mediaMaxMb` (default 5 MB).
Outbound send (text + media) [#outbound-send-text--media]
* Uses active web listener; error if gateway not running.
* Text chunking: 4k max per message (configurable via `channels.whatsapp.textChunkLimit`, optional `channels.whatsapp.chunkMode`).
* Media:
* Image/video/audio/document supported.
* Audio sent as PTT; `audio/ogg` => `audio/ogg; codecs=opus`.
* Caption only on first media item.
* Media fetch supports HTTP(S) and local paths.
* Animated GIFs: WhatsApp expects MP4 with `gifPlayback: true` for inline looping.
* CLI: `reeve message send --media --gif-playback`
* Gateway: `send` params include `gifPlayback: true`
Voice notes (PTT audio) [#voice-notes-ptt-audio]
WhatsApp sends audio as **voice notes** (PTT bubble).
* Best results: OGG/Opus. Reeve rewrites `audio/ogg` to `audio/ogg; codecs=opus`.
* `[[audio_as_voice]]` is ignored for WhatsApp (audio already ships as voice note).
Media limits + optimization [#media-limits--optimization]
* Default outbound cap: 5 MB (per media item).
* Override: `agents.defaults.mediaMaxMb`.
* Images are auto-optimized to JPEG under cap (resize + quality sweep).
* Oversize media => error; media reply falls back to text warning.
Heartbeats [#heartbeats]
* **Gateway heartbeat** logs connection health (`web.heartbeatSeconds`, default 60s).
* **Agent heartbeat** can be configured per agent (`agents.list[].heartbeat`) or globally
via `agents.defaults.heartbeat` (fallback when no per-agent entries are set).
* Uses the configured heartbeat prompt (default: `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`) + `HEARTBEAT_OK` skip behavior.
* Delivery defaults to the last used channel (or configured target).
Reconnect behavior [#reconnect-behavior]
* Backoff policy: `web.reconnect`:
* `initialMs`, `maxMs`, `factor`, `jitter`, `maxAttempts`.
* If maxAttempts reached, web monitoring stops (degraded).
* Logged-out => stop and require re-link.
Config quick map [#config-quick-map]
* `channels.whatsapp.dmPolicy` (DM policy: pairing/allowlist/open/disabled).
* `channels.whatsapp.selfChatMode` (same-phone setup; bot uses your personal WhatsApp number).
* `channels.whatsapp.allowFrom` (DM allowlist). WhatsApp uses E.164 phone numbers (no usernames).
* `channels.whatsapp.mediaMaxMb` (inbound media save cap).
* `channels.whatsapp.ackReaction` (auto-reaction on message receipt: `{emoji, direct, group}`).
* `channels.whatsapp.accounts..*` (per-account settings + optional `authDir`).
* `channels.whatsapp.accounts..mediaMaxMb` (per-account inbound media cap).
* `channels.whatsapp.accounts..ackReaction` (per-account ack reaction override).
* `channels.whatsapp.groupAllowFrom` (group sender allowlist).
* `channels.whatsapp.groupPolicy` (group policy).
* `channels.whatsapp.historyLimit` / `channels.whatsapp.accounts..historyLimit` (group history context; `0` disables).
* `channels.whatsapp.dmHistoryLimit` (DM history limit in user turns). Per-user overrides: `channels.whatsapp.dms[""].historyLimit`.
* `channels.whatsapp.groups` (group allowlist + mention gating defaults; use `"*"` to allow all)
* `channels.whatsapp.actions.reactions` (gate WhatsApp tool reactions).
* `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`)
* `messages.groupChat.historyLimit`
* `channels.whatsapp.messagePrefix` (inbound prefix; per-account: `channels.whatsapp.accounts..messagePrefix`; deprecated: `messages.messagePrefix`)
* `messages.responsePrefix` (outbound prefix)
* `agents.defaults.mediaMaxMb`
* `agents.defaults.heartbeat.every`
* `agents.defaults.heartbeat.model` (optional override)
* `agents.defaults.heartbeat.target`
* `agents.defaults.heartbeat.to`
* `agents.defaults.heartbeat.session`
* `agents.list[].heartbeat.*` (per-agent overrides)
* `session.*` (scope, idle, store, mainKey)
* `web.enabled` (disable channel startup when false)
* `web.heartbeatSeconds`
* `web.reconnect.*`
Logs + troubleshooting [#logs--troubleshooting]
* Subsystems: `whatsapp/inbound`, `whatsapp/outbound`, `web-heartbeat`, `web-reconnect`.
* Log file: `/tmp/reeve/reeve-YYYY-MM-DD.log` (configurable).
* Troubleshooting guide: [Gateway troubleshooting](/docs/gateway/troubleshooting).
Troubleshooting (quick) [#troubleshooting-quick]
**Not linked / QR login required**
* Symptom: `channels status` shows `linked: false` or warns βNot linkedβ.
* Fix: run `reeve channels login` on the gateway host and scan the QR (WhatsApp β Settings β Linked Devices).
**Linked but disconnected / reconnect loop**
* Symptom: `channels status` shows `running, disconnected` or warns βLinked but disconnectedβ.
* Fix: `reeve doctor` (or restart the gateway). If it persists, relink via `channels login` and inspect `reeve logs --follow`.
**Bun runtime**
* Bun is **not recommended**. WhatsApp (Baileys) and Telegram are unreliable on Bun.
Run the gateway with **Node**. (See Getting Started runtime note.)
# Zalo (Bot API) (/docs/channels/zalo)
Zalo (Bot API) [#zalo-bot-api]
Status: experimental. Direct messages only; groups coming soon per Zalo docs.
Plugin required [#plugin-required]
Zalo ships as a plugin and is not bundled with the core install.
* Install via CLI: `reeve plugins install @reeve/zalo`
* Or select **Zalo** during onboarding and confirm the install prompt
* Details: [Plugins](/plugin)
Quick setup (beginner) [#quick-setup-beginner]
1. Install the Zalo plugin:
* From a source checkout: `reeve plugins install ./extensions/zalo`
* From npm (if published): `reeve plugins install @reeve/zalo`
* Or pick **Zalo** in onboarding and confirm the install prompt
2. Set the token:
* Env: `ZALO_BOT_TOKEN=...`
* Or config: `channels.zalo.botToken: "..."`.
3. Restart the gateway (or finish onboarding).
4. DM access is pairing by default; approve the pairing code on first contact.
Minimal config:
```json5
{
channels: {
zalo: {
enabled: true,
botToken: "12345689:abc-xyz",
dmPolicy: "pairing"
}
}
}
```
What it is [#what-it-is]
Zalo is a Vietnam-focused messaging app; its Bot API lets the Gateway run a bot for 1:1 conversations.
It is a good fit for support or notifications where you want deterministic routing back to Zalo.
* A Zalo Bot API channel owned by the Gateway.
* Deterministic routing: replies go back to Zalo; the model never chooses channels.
* DMs share the agent's main session.
* Groups are not yet supported (Zalo docs state "coming soon").
Setup (fast path) [#setup-fast-path]
1) Create a bot token (Zalo Bot Platform) [#1-create-a-bot-token-zalo-bot-platform]
1. Go to **[https://bot.zaloplatforms.com](https://bot.zaloplatforms.com)** and sign in.
2. Create a new bot and configure its settings.
3. Copy the bot token (format: `12345689:abc-xyz`).
2) Configure the token (env or config) [#2-configure-the-token-env-or-config]
Example:
```json5
{
channels: {
zalo: {
enabled: true,
botToken: "12345689:abc-xyz",
dmPolicy: "pairing"
}
}
}
```
Env option: `ZALO_BOT_TOKEN=...` (works for the default account only).
Multi-account support: use `channels.zalo.accounts` with per-account tokens and optional `name`.
3. Restart the gateway. Zalo starts when a token is resolved (env or config).
4. DM access defaults to pairing. Approve the code when the bot is first contacted.
How it works (behavior) [#how-it-works-behavior]
* Inbound messages are normalized into the shared channel envelope with media placeholders.
* Replies always route back to the same Zalo chat.
* Long-polling by default; webhook mode available with `channels.zalo.webhookUrl`.
Limits [#limits]
* Outbound text is chunked to 2000 characters (Zalo API limit).
* Media downloads/uploads are capped by `channels.zalo.mediaMaxMb` (default 5).
* Streaming is blocked by default due to the 2000 char limit making streaming less useful.
Access control (DMs) [#access-control-dms]
DM access [#dm-access]
* Default: `channels.zalo.dmPolicy = "pairing"`. Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
* Approve via:
* `reeve pairing list zalo`
* `reeve pairing approve zalo `
* Pairing is the default token exchange. Details: [Pairing](/docs/start/pairing)
* `channels.zalo.allowFrom` accepts numeric user IDs (no username lookup available).
Long-polling vs webhook [#long-polling-vs-webhook]
* Default: long-polling (no public URL required).
* Webhook mode: set `channels.zalo.webhookUrl` and `channels.zalo.webhookSecret`.
* The webhook secret must be 8-256 characters.
* Webhook URL must use HTTPS.
* Zalo sends events with `X-Bot-Api-Secret-Token` header for verification.
* Gateway HTTP handles webhook requests at `channels.zalo.webhookPath` (defaults to the webhook URL path).
**Note:** getUpdates (polling) and webhook are mutually exclusive per Zalo API docs.
Supported message types [#supported-message-types]
* **Text messages**: Full support with 2000 character chunking.
* **Image messages**: Download and process inbound images; send images via `sendPhoto`.
* **Stickers**: Logged but not fully processed (no agent response).
* **Unsupported types**: Logged (e.g., messages from protected users).
Capabilities [#capabilities]
| Feature | Status |
| --------------- | ----------------------------- |
| Direct messages | β Supported |
| Groups | β Coming soon (per Zalo docs) |
| Media (images) | β Supported |
| Reactions | β Not supported |
| Threads | β Not supported |
| Polls | β Not supported |
| Native commands | β Not supported |
| Streaming | β οΈ Blocked (2000 char limit) |
Delivery targets (CLI/cron) [#delivery-targets-clicron]
* Use a chat id as the target.
* Example: `reeve message send --channel zalo --target 123456789 --message "hi"`.
Troubleshooting [#troubleshooting]
**Bot doesn't respond:**
* Check that the token is valid: `reeve channels status --probe`
* Verify the sender is approved (pairing or allowFrom)
* Check gateway logs: `reeve logs --follow`
**Webhook not receiving events:**
* Ensure webhook URL uses HTTPS
* Verify secret token is 8-256 characters
* Confirm the gateway HTTP endpoint is reachable on the configured path
* Check that getUpdates polling is not running (they're mutually exclusive)
Configuration reference (Zalo) [#configuration-reference-zalo]
Full configuration: [Configuration](/docs/gateway/configuration)
Provider options:
* `channels.zalo.enabled`: enable/disable channel startup.
* `channels.zalo.botToken`: bot token from Zalo Bot Platform.
* `channels.zalo.tokenFile`: read token from file path.
* `channels.zalo.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
* `channels.zalo.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. The wizard will ask for numeric IDs.
* `channels.zalo.mediaMaxMb`: inbound/outbound media cap (MB, default 5).
* `channels.zalo.webhookUrl`: enable webhook mode (HTTPS required).
* `channels.zalo.webhookSecret`: webhook secret (8-256 chars).
* `channels.zalo.webhookPath`: webhook path on the gateway HTTP server.
* `channels.zalo.proxy`: proxy URL for API requests.
Multi-account options:
* `channels.zalo.accounts..botToken`: per-account token.
* `channels.zalo.accounts..tokenFile`: per-account token file.
* `channels.zalo.accounts..name`: display name.
* `channels.zalo.accounts..enabled`: enable/disable account.
* `channels.zalo.accounts..dmPolicy`: per-account DM policy.
* `channels.zalo.accounts..allowFrom`: per-account allowlist.
* `channels.zalo.accounts..webhookUrl`: per-account webhook URL.
* `channels.zalo.accounts..webhookSecret`: per-account webhook secret.
* `channels.zalo.accounts..webhookPath`: per-account webhook path.
* `channels.zalo.accounts..proxy`: per-account proxy URL.
# Zalo Personal (unofficial) (/docs/channels/zalouser)
Zalo Personal (unofficial) [#zalo-personal-unofficial]
Status: experimental. This integration automates a **personal Zalo account** via `zca-cli`.
> **Warning:** This is an unofficial integration and may result in account suspension/ban. Use at your own risk.
Plugin required [#plugin-required]
Zalo Personal ships as a plugin and is not bundled with the core install.
* Install via CLI: `reeve plugins install @reeve/zalouser`
* Or from a source checkout: `reeve plugins install ./extensions/zalouser`
* Details: [Plugins](/plugin)
Prerequisite: zca-cli [#prerequisite-zca-cli]
The Gateway machine must have the `zca` binary available in `PATH`.
* Verify: `zca --version`
* If missing, install zca-cli (see `extensions/zalouser/README.md` or the upstream zca-cli docs).
Quick setup (beginner) [#quick-setup-beginner]
1. Install the plugin (see above).
2. Login (QR, on the Gateway machine):
* `reeve channels login --channel zalouser`
* Scan the QR code in the terminal with the Zalo mobile app.
3. Enable the channel:
```json5
{
channels: {
zalouser: {
enabled: true,
dmPolicy: "pairing"
}
}
}
```
4. Restart the Gateway (or finish onboarding).
5. DM access defaults to pairing; approve the pairing code on first contact.
What it is [#what-it-is]
* Uses `zca listen` to receive inbound messages.
* Uses `zca msg ...` to send replies (text/media/link).
* Designed for βpersonal accountβ use cases where Zalo Bot API is not available.
Naming [#naming]
Channel id is `zalouser` to make it explicit this automates a **personal Zalo user account** (unofficial). We keep `zalo` reserved for a potential future official Zalo API integration.
Finding IDs (directory) [#finding-ids-directory]
Use the directory CLI to discover peers/groups and their IDs:
```bash
reeve directory self --channel zalouser
reeve directory peers list --channel zalouser --query "name"
reeve directory groups list --channel zalouser --query "work"
```
Limits [#limits]
* Outbound text is chunked to \~2000 characters (Zalo client limits).
* Streaming is blocked by default.
Access control (DMs) [#access-control-dms]
`channels.zalouser.dmPolicy` supports: `pairing | allowlist | open | disabled` (default: `pairing`).
`channels.zalouser.allowFrom` accepts user IDs or names. The wizard resolves names to IDs via `zca friend find` when available.
Approve via:
* `reeve pairing list zalouser`
* `reeve pairing approve zalouser `
Group access (optional) [#group-access-optional]
* Default: `channels.zalouser.groupPolicy = "open"` (groups allowed). Use `channels.defaults.groupPolicy` to override the default when unset.
* Restrict to an allowlist with:
* `channels.zalouser.groupPolicy = "allowlist"`
* `channels.zalouser.groups` (keys are group IDs or names)
* Block all groups: `channels.zalouser.groupPolicy = "disabled"`.
* The configure wizard can prompt for group allowlists.
* On startup, Reeve resolves group/user names in allowlists to IDs and logs the mapping; unresolved entries are kept as typed.
Example:
```json5
{
channels: {
zalouser: {
groupPolicy: "allowlist",
groups: {
"123456789": { allow: true },
"Work Chat": { allow: true }
}
}
}
}
```
Multi-account [#multi-account]
Accounts map to zca profiles. Example:
```json5
{
channels: {
zalouser: {
enabled: true,
defaultAccount: "default",
accounts: {
work: { enabled: true, profile: "work" }
}
}
}
}
```
Troubleshooting [#troubleshooting]
**`zca` not found:**
* Install zca-cli and ensure itβs on `PATH` for the Gateway process.
**Login doesnβt stick:**
* `reeve channels status --probe`
* Re-login: `reeve channels logout --channel zalouser && reeve channels login --channel zalouser`
# acp (/docs/cli/acp)
acp [#acp]
Run the ACP (Agent Client Protocol) bridge that talks to a Reeve Gateway.
This command speaks ACP over stdio for IDEs and forwards prompts to the Gateway
over WebSocket. It keeps ACP sessions mapped to Gateway session keys.
Usage [#usage]
```bash
reeve acp
# Remote Gateway
reeve acp --url wss://gateway-host:18789 --token
# Attach to an existing session key
reeve acp --session agent:main:main
# Attach by label (must already exist)
reeve acp --session-label "support inbox"
# Reset the session key before the first prompt
reeve acp --session agent:main:main --reset-session
```
ACP client (debug) [#acp-client-debug]
Use the built-in ACP client to sanity-check the bridge without an IDE.
It spawns the ACP bridge and lets you type prompts interactively.
```bash
reeve acp client
# Point the spawned bridge at a remote Gateway
reeve acp client --server-args --url wss://gateway-host:18789 --token
# Override the server command (default: reeve)
reeve acp client --server "node" --server-args dist/entry.js acp --url ws://127.0.0.1:19001
```
How to use this [#how-to-use-this]
Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want
it to drive a Reeve Gateway session.
1. Ensure the Gateway is running (local or remote).
2. Configure the Gateway target (config or flags).
3. Point your IDE to run `reeve acp` over stdio.
Example config (persisted):
```bash
reeve config set gateway.remote.url wss://gateway-host:18789
reeve config set gateway.remote.token
```
Example direct run (no config write):
```bash
reeve acp --url wss://gateway-host:18789 --token
```
Selecting agents [#selecting-agents]
ACP does not pick agents directly. It routes by the Gateway session key.
Use agent-scoped session keys to target a specific agent:
```bash
reeve acp --session agent:main:main
reeve acp --session agent:design:main
reeve acp --session agent:qa:bug-123
```
Each ACP session maps to a single Gateway session key. One agent can have many
sessions; ACP defaults to an isolated `acp:` session unless you override
the key or label.
Zed editor setup [#zed-editor-setup]
Add a custom ACP agent in `~/.config/zed/settings.json` (or use Zedβs Settings UI):
```json
{
"agent_servers": {
"Reeve ACP": {
"type": "custom",
"command": "reeve",
"args": ["acp"],
"env": {}
}
}
}
```
To target a specific Gateway or agent:
```json
{
"agent_servers": {
"Reeve ACP": {
"type": "custom",
"command": "reeve",
"args": [
"acp",
"--url", "wss://gateway-host:18789",
"--token", "",
"--session", "agent:design:main"
],
"env": {}
}
}
}
```
In Zed, open the Agent panel and select βReeve ACPβ to start a thread.
Session mapping [#session-mapping]
By default, ACP sessions get an isolated Gateway session key with an `acp:` prefix.
To reuse a known session, pass a session key or label:
* `--session `: use a specific Gateway session key.
* `--session-label