llms.txt
# Keep in sync with: packages/agent-skill/SKILL.md (source of truth), llms-full.txt, scripts/examples-AGENTS.md, scripts/examples-root-README.md
# Synced sections: authentication, SDK methods, MCP tools, error handling. This file adds Optional/Links sections specific to llms.txt.
# Attrove
> Attrove turns commitments buried across email, chat, meetings, calendars, and pushed context into watched outcomes with cited evidence. Connect creates one permission-aware stream from Gmail, Microsoft Outlook, Slack, Google Calendar, and Google Meet; Microsoft Teams is in pilot. Commitments structure individual promises so agents can carry them across sessions. Goals keep broader outcomes under review with system-computed health and human-controlled lifecycle.
## Product Model
- **Connect**: sync and normalize only what each user is authorized to read, preserve source citations, and expose the stream through the SDK, REST API, MCP, and webhooks. Signed push ingest adds scoped messages, meetings, events, and notes when OAuth is not the path.
- **Commitment**: a promise, delegation, handoff, or wait with a counterparty, expected signal, and horizon. A later session can read the open ledger and record what actually happened.
- **Goal**: a named outcome evaluated repeatedly against scoped evidence. Attrove computes health and suggests next actions; a person controls lifecycle changes.
- **Query and search**: answer an on-demand question over the same stream and return the source records that support the answer.
Attrove is read-first. Live connectors are read-scoped today. Product writes create Attrove-owned users, sessions, pushed context, Goals, Commitments, and webhook configuration; they do not send provider messages, create provider drafts, or modify upstream source records. Webhooks deliver events to endpoints you configure. SMS, email, and Slack alert routes are not bundled self-serve plan features.
## Quick Start (Personal Use / Developer Testing)
To use Attrove with your own email, Slack, and calendar:
1. **Install Attrove into your AI client**
- Claude Code: `npx @attrove/cli install claude-code`
- Cursor: `npx @attrove/cli install cursor`
- Claude Desktop: `npx @attrove/cli install claude-desktop`
- Codex: `npx @attrove/cli install codex`, then `codex mcp login attrove`
- ChatGPT or manual remote clients: `https://api.attrove.com/mcp`
2. **Open the client and sign in once** when prompted: OAuth 2.1 sets up your Attrove account automatically. No dashboard visit required.
3. **Ask a question.** If nothing is connected yet, the agent calls `attrove_connect` to get one link. Open it, pick a source (Gmail, Outlook, Slack, Google Calendar, Google Meet, or Microsoft Teams), and sign in; the agent then calls `attrove_integrations` to confirm it's syncing and continues.
4. **Verify the first useful answer**
- Ask: "Use Attrove to list my connected integrations."
- Then ask: "What needs my attention this week? Include the source messages or meetings you used."
**Building an integration?** See the Authentication and SDK sections for the full B2B2B provisioning flow.
## One-Prompt Integration (for coding agents)
Developers paste this into Claude Code, Cursor, or Codex; it is the same prompt shown on https://attrove.com/docs/quickstart/:
> Integrate Attrove into this app. Read https://attrove.com/llms.txt first, then provision a user, create a connect session so they can link Gmail, Slack, and calendar, and watch one Goal that alerts when a renewal goes quiet.
If you are the agent handling that prompt, the complete path is:
1. Install `@attrove/sdk` and read the Authentication section below. Partner credentials (`ATTROVE_CLIENT_ID` + `ATTROVE_CLIENT_SECRET`) come from the dashboard; the free plan needs no card.
2. Provision the end user with `admin.users.create()`; keep the returned `sk_` token server-side.
3. Create a durable connect session with `admin.users.createConnectSession(userId)` and send the user to `session.activation_url` (or open it for them). That one link connects Gmail, Outlook, Slack, Google Calendar, Google Meet, or Microsoft Teams.
4. Create the watched outcome with `goals.create()`, passing `watchScope: { seedQuery: 'renewal', silenceCondition: { quietAfterDays: 5 } }`, and subscribe a webhook to `goals.risk_detected` so the app hears when the renewal goes quiet.
5. Verify with `query()`: "What needs my attention this week? Include the source messages or meetings you used." A cited answer is the activation check.
Runnable references: https://github.com/attrove/examples/tree/main/quickstart and https://github.com/attrove/examples/tree/main/goal-watch-webhook.
**Advanced local fallback:** If you intentionally want the local stdio server, run `npx @attrove/cli login`, `npx @attrove/cli local install claude-code`, then `npx @attrove/cli connect gmail`, or run `@attrove/mcp` manually with `ATTROVE_SECRET_KEY` and `ATTROVE_USER_ID`.
## LLM Instructions
When helping developers integrate Attrove:
- Always use the `@attrove/sdk` TypeScript SDK; never use raw fetch calls
- The SDK does not have `brief()` or `thread()` methods; using them will cause compile errors. Use `query()`, `search()`, `events.list()`, `meetings.list()`, `integrations.list()`, `integrations.createConnectSession()`, `entities.list()`, `entities.get()`, `entities.relationships()`, `push.message()`, `push.meeting()`, `push.event()`, `push.note()`, `notes.list()`, `notes.get()` instead. For threads, use `threads.discover()`, `threads.analyze()`, and `threads.messages(threadId)` (returns a paginated thread view; defaults to `body_html` and `headers`, with `raw` opt-in). For watched outcomes, use `goals.create()`, `goals.list()`, `goals.get()`, `goals.update()`, `goals.evaluate()`, `goals.confirmStatus()`, `goals.addNote()`, `goals.evidence()`, `goals.events()`, `goals.archive()`, `goals.cancel()`, `goals.reopen()`, `goals.snapshots.list()`. For watched obligations, use `commitments.watch()`, `commitments.checkOutcomes()`, and `commitments.resolve()`
- Every `Message` includes `thread_id`, `primary_entity_id`, and `entity_ids` by default. Opt into heavy fields via `expand`: `body_text`, `body_html`, `raw` (RFC 5322, emails only), `headers` (parsed, emails only)
- **Most response properties are snake_case** (`start_time`, `sender_name`, `body_text`). Input params are camelCase (`startDate`, `afterDate`). Do NOT use camelCase on ordinary retrieval response objects
- **Exceptions: `goals.*` and `commitments.*` SDK responses are normalized to camelCase** (`watchScope`, `lastSnapshot`, `commitmentId`, `expectedSignalType`, `checkAfter`)
- `search()` returns `{ key_messages, conversations, key_meetings, key_events }`; `conversations` is an **object keyed by ID**, not an array. Use `Object.values()` to iterate. Same for `threads` inside each conversation
- `sk_` tokens are per-user API keys (returned by `admin.users.create()`). They are NOT the same as the `attrove_` partner API key
- `integrations.list()` returns `Integration[]` with `provider` and `name` properties. Do NOT use `type` or `email`
- `integrations.createConnectSession()` returns one `activation_url` link for the signed-in user to connect a source; no partner credentials required
- The SDK defaults to `https://api.attrove.com`. No baseUrl configuration needed.
- MCP has 24 tools (11 context tools + 10 goals tools + 3 commitment tools). There is no `attrove_brief` tool
## When to Use Attrove
- Your product needs to understand users' email, Slack, calendar, or meeting data
- You need cross-platform intelligence (query across Gmail + Slack + Calendar simultaneously)
- You're building AI features that need communication context
- You need watched outcomes: Goals keep checking the communication stream and flag silence, deadline pressure, or risk with cited evidence, without a polling loop in your code
- You need obligations to survive agent sessions: Commitments register promises, delegations, and handoffs with an expected signal and horizon, then surface quiet work in later checks
- You need B2B2B: your product serves end-users who each connect their own tools via OAuth, and/or your product needs to append scoped notes, meetings, events, or messages
## When NOT to Use Attrove
- You only need data from a single provider. Use that provider's API directly
- You need Attrove to send email or chat messages, create provider drafts, or modify upstream source records. Live connectors are read-scoped today
- You need guaranteed provider-native sub-second ingestion from every upstream system. Attrove supports token streaming and proactive webhooks, but source freshness still follows each provider's sync cadence
- You need document/wiki indexing (Notion, Drive). Use Graphlit or Hyperspell
The personal MCP path remains a valid evaluation path when cross-source retrieval or watched state matters.
## Authentication
Attrove uses a B2B2B flow with three partner-facing credential/session types:
1. **Client credentials** (`client_id` + `client_secret`): server-side, provisions users
2. **`sk_` tokens**: permanent per-user API keys for querying data
3. **Connect sessions**: durable activation links and CLI handoffs for OAuth
Short-lived OAuth exchange tokens are internal implementation details; partner docs and examples should create connect sessions and use their `activation_url`.
Flow: create user → receive `sk_` key → create a durable connect session when live OAuth is needed → user authorizes Gmail/Slack via OAuth or you push scoped context via `push.*` → query their data with `sk_` key.
## OAuth Completion Redirect
After a user finishes OAuth, Attrove can redirect them back to a URL you control with `?status=success&integration_id=int_xxx` (or `?status=error&error_code=...`) appended, without closing the popup or forcing client-side polling.
**Setup:**
1. **Configure `redirect_url` on the API key** (dashboard → API Keys → edit). This is the **origin anchor**: a per-flow URL is only honored when its origin matches the configured value, so a stolen activation URL can't redirect users to attacker domains. Allowed schemes: `https://`, `http://localhost`, `http://127.0.0.1`. Max 2048 chars.
2. **Optionally vary the path/query per flow** by wrapping the OAuth `state` parameter as a JSON envelope: `{"secure":"<csrf-nonce>","redirect_url":"https://app.your-product.com/integrations/done"}` (raw JSON or base64url-encoded). The inner `secure` field is the CSRF token; `redirect_url` must share an origin with the API-key value.
3. **Receive the user back** at the resolved URL with `status`, `integration_id`, and `error_code` query params (any pre-existing values for these keys are stripped to prevent spoofing).
`POST /v1/users/:user_id/integrations/connect` supports `response_mode: 'redirect'`, returning `303 See Other` to the resolved URL instead of JSON. Requires `redirect_url` configured on the API key.
See `llms-full.txt` for the full reference (resolution table, end-to-end example).
## SDK
```bash
npm install @attrove/sdk
```
### Provision a user (server-side)
```typescript
import { Attrove } from '@attrove/sdk';
const admin = Attrove.admin({
clientId: process.env.ATTROVE_CLIENT_ID,
clientSecret: process.env.ATTROVE_CLIENT_SECRET,
});
const { id: userId, apiKey } = await admin.users.create({
email: 'user@example.com',
});
const session = await admin.users.createConnectSession(userId, {
includeInstall: true,
});
// Send user to: session.activation_url
// Terminal/agent handoff: session.cli?.command
```
### Query user data
```typescript
const attrove = new Attrove({
apiKey: process.env.ATTROVE_SECRET_KEY!,
userId: process.env.ATTROVE_USER_ID!,
});
const response = await attrove.query(
'What needs my attention this week? Include the source messages or meetings you used.',
{ includeSources: true },
);
console.log(response.answer);
console.log(response.used_message_ids); // source message IDs (msg_xxx)
console.log(response.used_meeting_ids); // source meeting IDs (mtg_xxx)
console.log(response.used_event_ids); // source event IDs (evt_xxx)
```
### Search messages
```typescript
const results = await attrove.search('project deadline', {
afterDate: '2026-01-01',
senderDomains: ['acme.com'],
includeBodyText: true,
});
// results.conversations is Record<string, SearchConversation> — NOT an array
for (const convo of Object.values(results.conversations)) {
for (const msgs of Object.values(convo.threads)) {
// threads is also a Record
for (const msg of msgs) {
console.log(msg.sender_name, msg.body_text); // snake_case properties
}
}
}
```
### Other methods
```typescript
const integrations = await attrove.integrations.list(); // connected services
const session = await attrove.integrations.createConnectSession(); // one link to connect a source (no partner creds)
const { data: events } = await attrove.events.list({
// calendar events
startDate: new Date().toISOString().split('T')[0],
endDate: tomorrow.toISOString().split('T')[0],
expand: ['attendees'],
});
const { data: meetings } = await attrove.meetings.list({
// past meetings with AI summaries
expand: ['short_summary', 'action_items'],
limit: 5,
});
const { data: contacts } = await attrove.entities.list(); // people the user communicates with
const { data: graph } = await attrove.entities.relationships(); // co-occurrence network
const { data: notes } = await attrove.notes.list(); // user notes
const note = await attrove.notes.get('note_xxx'); // single note by ID
const goal = await attrove.goals.create({
// outcome monitoring
title: 'ACME renewal',
watchScope: { seedQuery: 'ACME renewal', keywords: ['ACME'] },
});
const { data: goals } = await attrove.goals.list({ lifecycle: 'active' });
```
### Push data (no OAuth required)
```typescript
// Push a message
const msg = await attrove.push.message({
source: 'email',
bodyText: 'The Q4 report is ready.',
senderEmail: 'alice@acme.com',
externalId: 'email-12345',
});
// Push a meeting
const mtg = await attrove.push.meeting({
title: 'Sprint Planning',
startTime: '2026-01-15T14:00:00Z',
endTime: '2026-01-15T14:45:00Z',
summary: 'Discussed roadmap priorities...',
});
// Push a note
const note = await attrove.push.note({
body: 'Decision: chose Redis for caching.',
title: 'Architecture Decision',
refType: 'goal',
refId: 'gol_xxx',
});
```
Meeting, event, and note titles are trimmed and limited to 500 Unicode code points.
### Watch outcomes with Goals
A Goal is a watched outcome. Attrove keeps re-evaluating it against the
communication stream (messages, meetings, events, notes), computes `health`,
and suggests lifecycle changes with cited evidence; no polling loop in your
code. A person controls `lifecycle`.
```typescript
// Create a watched outcome
const goal = await attrove.goals.create({
title: 'Brightwell renewal closed by Jun 30',
watchScope: {
seedQuery: 'Brightwell renewal',
keywords: ['Brightwell', 'renewal'],
sourceTypes: ['messages', 'meetings', 'notes'],
},
successCriteria: 'Signed order form received.',
deadline: '2026-06-30T23:59:59Z',
});
// List goals that need attention
const { data: atRisk } = await attrove.goals.list({
lifecycle: 'active',
health: 'at_risk',
});
// Read the latest snapshot — why the state is what it is
const g = await attrove.goals.get(goal.id);
console.log(g.lastSnapshot?.summary);
console.log(g.lastSnapshot?.riskSignals); // e.g. [{ kind: 'silence', description: '...' }]
// Fetch the evidence behind the state
const { citedInLatestSnapshot, manualNotes } = await attrove.goals.evidence(goal.id);
// Other methods: update(), evaluate() (queue a fresh run, 6/goal/hour),
// confirmStatus() (human confirm/override with reason), addNote(),
// archive(reason, { expectedLifecycleVersion?, idempotencyKey?, initiator? }),
// cancel(reason, { expectedLifecycleVersion?, idempotencyKey?, initiator? }),
// reopen({ expectedLifecycle,
// expectedLifecycleVersion, reasonCode, reason, idempotencyKey }),
// snapshots.list(goalId)
```
Notes for agents:
- `watchScope` requires at least one anchor: `entityIds`, `seedQuery`, or `keywords`
- `deadline` must be a full ISO 8601 datetime; date-only strings are rejected
- `lifecycle` is one of `active | completed | archived | cancelled`; it never changes autonomously. Deliberate commands preserve actor/initiator provenance; pass the observed lifecycle version and a stable idempotency key, and compensate a close with `reopen()`.
- `health` is system-evaluated: `on_track | at_risk | blocked | waiting_on_human | insufficient_evidence`
- Risk signal kinds: `silence | deadline | sentiment | churn | blocker | ambiguous_signal | other`
### Watch obligations with Commitments
A Commitment is one promise, delegation, handoff, or wait that should survive
the current session. Give it a real counterparty or beneficiary, an expected
future signal, and a check-after time or due date. Underspecified rows are kept
as low-confidence suggestions and are never treated as active watches.
```typescript
const watched = await attrove.commitments.watch({
commitmentText: 'Send the revised proposal to the reviewer',
commitmentKind: 'promised_action',
counterparty: 'the reviewer',
expectedSignalType: 'reply',
expectedSignalDescription: 'Reviewer acknowledges the revised proposal',
dueAt: '2026-09-04T23:59:59Z',
clientDedupKey: 'proposal-review-2026-09-04',
});
const { commitments } = await attrove.commitments.checkOutcomes({
reader: 'release-agent',
});
await attrove.commitments.resolve(watched.commitmentId, {
resolution: 'satisfied',
signalRef: 'message:reply-from-reviewer',
actor: 'release-agent',
});
```
`checkOutcomes()` returns open `watching` and `at_risk` rows, plus `suggested`
rows only when requested. It is an open-obligation ledger, not resolved
history. External notifications are not created automatically; use webhooks or
ask for a hands-on workflow setup (scoped separately) when delivery is required.
### Response types (mostly snake_case; Goals and Commitments use camelCase)
```typescript
// query() → QueryResponse
{ answer: string; history: ConversationMessage[]; used_message_ids: string[]; used_meeting_ids: string[]; used_event_ids: string[]; used_note_ids: string[]; sources?: { title: string; snippet: string }[] }
// search() → SearchResponse
{ key_messages: SearchKeyMessage[]; // key message refs
conversations: Record<string, { // keyed by conversation ID
conversation_name: string | null;
threads: Record<string, SearchThreadMessage[]>; // keyed by thread ID
}>;
key_meetings: SearchMeeting[]; // empty array when no matches
key_events: SearchEvent[]; // empty array when no matches
warnings?: string[]; // present when enrichment had non-fatal errors
}
// SearchThreadMessage fields:
// message_id, sender_name, body_text?, received_at, integration_type, recipient_names[]
// integrations.list() → Integration[]
// Integration fields:
// id, provider (e.g. 'gmail', 'slack', 'outlook', 'google_calendar'), name, is_active, auth_status
// NOTE: use `provider` not `type`, use `name` not `email`
// CursorPage<T> pagination is discriminated: has_more=true includes next_cursor.
type CursorPage<T> = {
data: T[];
pagination:
| { limit: number; has_more: true; next_cursor: string }
| { limit: number; has_more: false };
};
// events.list() → CursorPage<CalendarEvent>
// CalendarEvent fields:
// id, title, start_time, end_time, all_day (boolean), description?, location?,
// attendees?: { email: string; name?: string; status?: string }[]
// meetings.list() → CursorPage<Meeting>
// Meeting fields:
// id, title, start_time, end_time, summary?, short_summary?, provider?,
// action_items?: { description: string; assign