@attrove/sdk
Official TypeScript SDK for the Attrove API. Attrove watches work across your users' email, chat, meetings, and calendar — when something starts to slip, it flags it with proof. Create watched outcomes (Goals) that catch the conversations going quiet, and query the same communication stream with cited evidence.
Installation
npm install @attrove/sdk
yarn add @attrove/sdk
pnpm add @attrove/sdk
Quick Start
import { Attrove } from '@attrove/sdk';
const attrove = new Attrove({
apiKey: 'sk_...',
userId: 'user-uuid'
});
const goal = await attrove.goals.create({
title: 'Acme renewal closed by Jun 30',
watchScope: {
seedQuery: 'Acme renewal',
keywords: ['Acme', 'renewal'],
silenceCondition: { quietAfterDays: 5, alertHealth: 'at_risk' },
},
successCriteria: 'Signed order form received.',
});
const response = await attrove.query('What meetings do I have tomorrow?');
console.log(response.answer);
const results = await attrove.search('quarterly report');
Core Methods
query(prompt, options?)
Ask questions about the user's unified context with AI-generated answers.
const response = await attrove.query('What did Sarah say about the Q4 budget?');
console.log(response.answer);
console.log(response.used_message_ids);
console.log(response.used_meeting_ids);
console.log(response.used_event_ids);
let history = response.history;
const followUp = await attrove.query('What about Q3?', { history });
history = followUp.history;
const filtered = await attrove.query('Latest updates', {
integrationIds: ['int_xxx'],
includeSources: true
});
const custom = await attrove.query('Compare Alice and Bob on budget adherence.', {
instructions: 'Return a markdown table with columns: Person, On-Track, Key Evidence.',
context: 'FY26 budget: Engineering $2M, Marketing $800K. Alice owns Engineering, Bob owns Marketing.',
});
search(query, options?)
Semantic search that returns raw matches across messages, meetings, and events without AI summarization.
const results = await attrove.search('product launch', {
afterDate: '2024-01-01',
senderDomains: ['acme.com'],
includeBodyText: true
});
for (const [convId, conv] of Object.entries(results.conversations)) {
console.log(`Conversation: ${conv.conversation_name}`);
}
Resource Namespaces
Goals (watched outcomes)
A Goal is a watched outcome — a deal, renewal, follow-up, or deliverable that should resolve. Attrove keeps re-evaluating it against the communication stream and updates its health (on_track, at_risk, blocked, waiting_on_human, insufficient_evidence) with cited evidence. Add a silenceCondition and Attrove flags the goal when the conversation goes quiet — no polling loop in your code.
const goal = await attrove.goals.create({
title: 'Acme Corp pilot decision',
watchScope: {
seedQuery: 'Acme Corp pilot',
keywords: ['Acme Corp', 'pilot'],
sourceTypes: ['messages', 'meetings', 'notes'],
silenceCondition: { quietAfterDays: 5, alertHealth: 'at_risk' },
},
successCriteria: 'Pilot agreement signed.',
});
const { data: atRisk } = await attrove.goals.list({
lifecycle: 'active',
health: 'at_risk',
});
const { runId } = await attrove.goals.evaluate(goal.id);
const current = await attrove.goals.get(goal.id);
console.log(current.lastRun?.status);
await attrove.goals.addNote(goal.id, {
title: 'Phone call',
body: 'Buyer asked for procurement timeline.',
});
const evidence = await attrove.goals.evidence(goal.id);
const events = await attrove.goals.events({
types: ['goals.created', 'goals.risk_detected', 'goals.completed'],
limit: 10,
});
console.log(events.watermark);
const snapshots = await attrove.goals.snapshots.list(goal.id, { limit: 5 });
Lifecycle never changes autonomously — humans confirm via confirmStatus(). Pair Goals with the goals.risk_detected webhook (see below) to route alerts wherever the owner works.
Users
const { user, integrations } = await attrove.users.get();
await attrove.users.update({
timezone: 'America/New_York'
});
const stats = await attrove.users.syncStats();
console.log(`Messages: ${stats.totals.messages.count}`);
Messages
const { data, pagination } = await attrove.messages.list({
limit: 20,
expand: ['body_text']
});
const { data: messages } = await attrove.messages.list({
ids: response.used_message_ids,
expand: ['body_text']
});
const message = await attrove.messages.get('message-uuid');
Conversations
const { data: conversations } = await attrove.conversations.list({
syncedOnly: true
});
await attrove.conversations.updateSync([
{ id: 'conversation-uuid-1', importMessages: true },
{ id: 'conversation-uuid-2', importMessages: false }
]);
Integrations
const integrations = await attrove.integrations.list();
const integration = await attrove.integrations.get('integration-id');
console.log(`${integration.provider}: last synced ${integration.last_synced_at}`);
await attrove.integrations.disconnect('integration-uuid');
Threads
const { threads } = await attrove.threads.discover('Q4 budget discussion', {
integrationTypes: ['slack'],
afterDate: '2024-01-01',
limit: 5,
});
for (const thread of threads) {
console.log(`${thread.title} (score: ${thread.relevance_score})`);
}
const analysis = await attrove.threads.analyze('conversation-uuid');
console.log(analysis.summary);
console.log(`Sentiment: ${analysis.sentiment}`);
console.log(`Action items: ${analysis.action_items.length}`);
console.log(`Decisions: ${analysis.decisions.length}`);
const page = await attrove.threads.messages('conversation-uuid');
Meetings
const { data: meetings } = await attrove.meetings.list({
expand: ['summary', 'action_items', 'attendees'],
});
const meeting = await attrove.meetings.get('meeting-id');
const updated = await attrove.meetings.update('meeting-id', {
summary: 'Revised meeting summary.',
shortSummary: 'Brief revision.',
actionItems: [
{ description: 'Follow up with client', assignee: 'Alice' },
],
});
const result = await attrove.meetings.regenerateSummary('meeting-id');
console.log(result.summary);
console.log(`Action items: ${result.action_items.length}`);
await attrove.meetings.delete({ externalId: 'fireflies-meeting-123' });
Events
const { data: events } = await attrove.events.list({
startDate: '2026-01-01',
endDate: '2026-01-31',
expand: ['attendees', 'description'],
});
const event = await attrove.events.get('evt_abc123');
Entities
const { data: entities } = await attrove.entities.list({
search: 'Alice',
isBot: false,
});
const entity = await attrove.entities.get('ent_abc123');
const { data: relationships } = await attrove.entities.relationships({
limit: 50,
});
Notes
const { data: notes } = await attrove.notes.list({ limit: 20 });
const goalNotes = await attrove.notes.list({
refType: 'goal',
refId: 'gol_abc123',
});
const note = await attrove.notes.get('note_abc123');
await attrove.notes.delete({ id: 'note_abc123' });
Push
const pushed = await attrove.push.note({
title: 'Customer call',
body: 'CFO asked for ROI math before next review.',
refType: 'goal',
refId: 'gol_abc123',
externalId: 'crm-note-123',
});
console.log(pushed.status);
Server-to-Server (Admin) API
Use the admin client for operations that require partner authentication:
import { Attrove } from '@attrove/sdk';
const admin = Attrove.admin({
clientId: 'your-client-id',
clientSecret: 'your-client-secret'
});
const { id, apiKey } = await admin.users.create({
email: 'user@example.com',
firstName: 'John',
lastName: 'Doe'
});
const session = await admin.users.createConnectSession(id, {
provider: 'gmail',
includeInstall: true,
});
const attrove = new Attrove({ apiKey, userId: id });
console.log(session.activation_url);
console.log(session.cli?.command);
const endpoint = await admin.webhooks.create({
url: 'https://your-app.example.com/webhooks/attrove',
eventTypes: ['messages.new', 'notes.new', 'goals.risk_detected'],
userIds: [id],
});
console.log(endpoint.secret);
After an integration is connected, verify the first useful answer:
const answer = await attrove.query(
'What needs my attention this week? Include the source messages or meetings you used.',
{ includeSources: true },
);
console.log(answer.answer);
Webhook deliveries are CloudEvents JSON and are signed with webhook-id, webhook-timestamp, and webhook-signature headers. Use verifyWebhookSignature from @attrove/sdk against the raw request body before JSON parsing, then parseWebhookEvent to get a fully typed event:
import express from 'express';
import { parseWebhookEvent, verifyWebhookSignature } from '@attrove/sdk';
const app = express();
app.post(
'/webhooks/attrove',
express.raw({ type: 'application/cloudevents+json' }),
(req, res) => {
if (!verifyWebhookSignature(req.headers, req.body, process.env.ATTROVE_WEBHOOK_SECRET!)) {
return res.status(401).end();
}
const event = parseWebhookEvent(req.body);
switch (event.type) {
case 'goals.next_move_changed': {
const { goal, next_move, previous_owner } = event.data;
if (next_move?.owner === 'us') {
console.log(`Your move on "${goal.title}": ${next_move.obligation}`);
} else if (next_move === null) {
console.log(`"${goal.title}" has no open move (was: ${previous_owner})`);
}
break;
}
case 'goals.risk_detected':
console.log(`At risk: ${event.data.goal.title}`);
break;
case 'unknown_event':
break;
}
res.status(204).end();
},
);
parseWebhookEvent never throws on an unrecognized event type — it returns the unknown_event member instead (as it also does for a recognized type whose envelope is malformed), so API-side event additions cannot break your receiver. For a recognized type, the data payload is returned with its declared type but is not deeply field-validated (only the envelope is): null-check nested fields (next_move?.owner, data.snapshot_id) rather than assuming a declared field is present, since server-side drift could omit one.
Error Handling
The SDK provides typed errors for better error handling:
import {
Attrove,
AttroveError,
AuthenticationError,
NotFoundError,
RateLimitError
} from '@attrove/sdk';
try {
const response = await attrove.query('...');
} catch (error) {
if (error instanceof AuthenticationError) {
console.log('Invalid API key');
} else if (error instanceof RateLimitError) {
console.log(`Rate limited. Retry after ${error.retryAfter}s`);
} else if (error instanceof NotFoundError) {
console.log('Resource not found');
} else if (error instanceof AttroveError) {
console.log(`Error: ${error.code} - ${error.message}`);
}
}
Streaming (Advanced)
For real-time streaming of query responses:
const result = await attrove.stream('What happened in the meeting?', {
onChunk: (chunk) => process.stdout.write(chunk),
onState: (state) => console.log('State:', state),
onEnd: (reason) => console.log('Stream ended:', reason)
});
console.log('Full answer:', result.answer);
Note: Streaming uses WebSocket connections and requires the same API key authentication as other SDK methods. It is primarily intended for end-user facing applications where progressive display of responses improves the user experience.
Configuration
const attrove = new Attrove({
apiKey: 'sk_...',
userId: 'user-uuid',
baseUrl: 'https://api.attrove.com',
timeout: 30000,
maxRetries: 3
});
TypeScript Support
The SDK is fully typed. Import types as needed:
import {
QueryOptions,
QueryResponse,
SearchOptions,
SearchResponse,
User,
Message,
Integration,
ConversationMessage,
Goal,
GoalEventsPage
} from '@attrove/sdk';
Requirements
- Node.js 18.0.0 or later
- TypeScript 4.7+ (if using TypeScript)
License
MIT