Overview
This guide shows you how to build a complete AI scheduling assistant similar to x.ai or Clara - an agent that can schedule meetings autonomously through natural conversation.Architecture
Implementation
Step 1: User Onboarding
First, users need to connect their Google Calendar:Copy
// 1. Get OAuth URL from Syncline
const response = await fetch('https://api.syncline.run/v1/platform/oauth-url', {
method: 'GET',
headers: {
'X-API-Key': process.env.SYNCLINE_API_KEY
},
params: {
user_email: user.email,
redirect_uri: 'https://your-app.com/oauth/callback',
state: user.id // Track who's connecting
}
});
const { oauth_url } = await response.json();
// 2. Redirect user to OAuth URL
res.redirect(oauth_url);
// 3. Handle callback
app.get('/oauth/callback', (req, res) => {
const { state, success } = req.query;
if (success === 'true') {
const userId = state;
// User connected! Mark in your database
db.updateUser(userId, { calendar_connected: true });
res.redirect('/dashboard?message=Calendar connected!');
} else {
res.redirect('/dashboard?error=Connection failed');
}
});
Step 2: Natural Language Processing
Use an LLM to understand user intent:Copy
const OpenAI = require('openai');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function parseIntent(userMessage, userEmail) {
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: `You are a scheduling assistant. Parse user requests into structured actions.
Available actions:
- schedule_meeting: Schedule a new meeting
- find_availability: Find available times
- reschedule_meeting: Move an existing meeting
- cancel_meeting: Cancel a meeting
- update_preferences: Change scheduling preferences
Extract:
- action: The action type
- attendees: List of email addresses
- duration: Meeting duration in minutes
- title: Meeting title
- description: Meeting description
- context: Type of meeting (investor_call, quick_sync, etc.)
- date_preference: Any time preferences mentioned
Return JSON only.`
},
{
role: 'user',
content: `User (${userEmail}): ${userMessage}`
}
],
response_format: { type: 'json_object' }
});
return JSON.parse(completion.choices[0].message.content);
}
Step 3: Execute Scheduling Actions
Copy
async function handleSchedulingRequest(intent, userEmail) {
switch (intent.action) {
case 'schedule_meeting':
return await scheduleMeeting(intent, userEmail);
case 'find_availability':
return await findAvailability(intent, userEmail);
case 'reschedule_meeting':
return await rescheduleMeeting(intent, userEmail);
case 'cancel_meeting':
return await cancelMeeting(intent);
case 'update_preferences':
return await updatePreferences(intent, userEmail);
default:
return { error: 'Unknown action' };
}
}
async function scheduleMeeting(intent, userEmail) {
// Use auto-scheduling for fully autonomous operation
const response = await fetch('https://api.syncline.run/v1/schedule/auto', {
method: 'POST',
headers: {
'X-API-Key': process.env.SYNCLINE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
attendees: [userEmail, ...intent.attendees],
duration_minutes: intent.duration || 30,
title: intent.title,
description: intent.description,
context: intent.context || 'general',
auto: true
})
});
const meeting = await response.json();
if (!response.ok) {
// Handle user not connected
if (meeting.code === 'USER_NOT_CONNECTED') {
return {
success: false,
message: `${meeting.user_email} hasn't connected their calendar yet. I'll send them an invite link.`,
oauth_url: meeting.oauth_url
};
}
throw new Error(meeting.message);
}
return {
success: true,
meeting,
message: `Perfect! I've scheduled "${meeting.title}" for ${new Date(meeting.start_time).toLocaleString()}. Google Meet link: ${meeting.meet_link}`
};
}
async function findAvailability(intent, userEmail) {
const response = await fetch('https://api.syncline.run/v1/availability', {
method: 'POST',
headers: {
'X-API-Key': process.env.SYNCLINE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
attendees: [userEmail, ...intent.attendees],
duration_minutes: intent.duration || 30
})
});
const { slots } = await response.json();
// Present top 3 options
const topSlots = slots.slice(0, 3);
const options = topSlots.map((slot, i) => ({
number: i + 1,
time: new Date(slot.start_time).toLocaleString(),
day: slot.day_of_week,
score: slot.score,
quality: slot.match_quality
}));
return {
success: true,
options,
message: `Found ${slots.length} times. Here are the top 3:\n` +
options.map(o => `${o.number}. ${o.day} at ${o.time} (${o.quality})`).join('\n') +
`\nWhich works best?`
};
}
Step 4: Conversation Flow
Copy
class SchedulingAgent {
constructor(userId) {
this.userId = userId;
this.conversationHistory = [];
this.pendingAction = null;
}
async handleMessage(userMessage) {
// Add to history
this.conversationHistory.push({
role: 'user',
content: userMessage
});
// Check if following up on previous action
if (this.pendingAction) {
return await this.handleFollowUp(userMessage);
}
// Parse new intent
const user = await db.getUser(this.userId);
const intent = await parseIntent(userMessage, user.email);
// Execute action
const result = await handleSchedulingRequest(intent, user.email);
// Store if needs follow-up (e.g., user needs to pick from options)
if (result.options) {
this.pendingAction = {
type: 'choose_slot',
options: result.options,
intent
};
}
// Add to history
this.conversationHistory.push({
role: 'assistant',
content: result.message
});
return result;
}
async handleFollowUp(userMessage) {
if (this.pendingAction.type === 'choose_slot') {
// User picked a slot number
const choice = parseInt(userMessage);
if (isNaN(choice) || choice < 1 || choice > this.pendingAction.options.length) {
return {
success: false,
message: 'Please pick a number from the options above.'
};
}
const chosenSlot = this.pendingAction.options[choice - 1];
const user = await db.getUser(this.userId);
// Schedule at chosen time
const response = await fetch('https://api.syncline.run/v1/schedule', {
method: 'POST',
headers: {
'X-API-Key': process.env.SYNCLINE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
attendees: [user.email, ...this.pendingAction.intent.attendees],
start_time: chosenSlot.time,
duration_minutes: this.pendingAction.intent.duration,
title: this.pendingAction.intent.title
})
});
const meeting = await response.json();
this.pendingAction = null; // Clear pending
return {
success: true,
meeting,
message: `Scheduled! ${meeting.title} on ${new Date(meeting.start_time).toLocaleString()}. Meet link: ${meeting.meet_link}`
};
}
}
}
Step 5: Webhook Integration
Listen for real-time events:Copy
const crypto = require('crypto');
app.post('/webhooks/syncline', express.json(), (req, res) => {
// Verify signature
const signature = req.headers['x-syncline-signature'];
const secret = process.env.SYNCLINE_WEBHOOK_SECRET;
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(JSON.stringify(req.body))
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Return 200 immediately
res.status(200).send('OK');
// Process event asynchronously
const { event, data } = req.body;
switch (event) {
case 'user.calendar_connected':
console.log(`${data.user_email} connected their calendar`);
// Send welcome email, update UI, etc.
sendWelcomeEmail(data.user_email);
break;
case 'meeting.scheduled':
console.log(`Meeting scheduled: ${data.meeting_id}`);
// Send confirmation to users
sendMeetingConfirmation(data);
break;
case 'meeting.rescheduled':
console.log(`Meeting rescheduled: ${data.learned_pattern}`);
// Notify users, log the learning
notifyReschedule(data);
break;
case 'meeting.cancelled':
console.log(`Meeting cancelled: ${data.meeting_id}`);
// Update CRM, notify stakeholders
handleCancellation(data);
break;
}
});
async function sendMeetingConfirmation(meeting) {
// Send email or in-app notification
await sendEmail({
to: meeting.attendees,
subject: `Meeting Confirmed: ${meeting.title}`,
body: `
Your meeting "${meeting.title}" is confirmed!
When: ${new Date(meeting.start_time).toLocaleString()}
Duration: ${meeting.duration_minutes} minutes
Join: ${meeting.meet_link}
This meeting was intelligently scheduled based on your preferences.
Quality score: ${meeting.match_quality} (${meeting.score.toFixed(2)})
`
});
}
Complete Example: Sales AI Agent
Copy
// AI agent that schedules demos for sales team
class SalesDemoAgent {
async scheduleDemo(leadEmail, leadName, salesRepEmail, notes) {
// 1. Check if lead has connected calendar
const status = await this.checkConnectionStatus(leadEmail);
if (!status.connected) {
// Send calendar connection request
const oauthUrl = await this.getOAuthUrl(leadEmail);
await this.sendConnectionRequest(leadEmail, leadName, oauthUrl);
return {
status: 'pending_connection',
message: `Calendar connection request sent to ${leadName}`
};
}
// 2. Lead is connected - schedule automatically
const result = await fetch('https://api.syncline.run/v1/schedule/auto', {
method: 'POST',
headers: {
'X-API-Key': process.env.SYNCLINE_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
attendees: [salesRepEmail, leadEmail],
duration_minutes: 30,
title: `Product Demo with ${leadName}`,
description: notes || 'Product demonstration and Q&A',
context: 'sales_demo',
auto: true
})
});
const meeting = await result.json();
// 3. Log in CRM
await this.logMeetingInCRM({
lead_email: leadEmail,
sales_rep: salesRepEmail,
meeting_id: meeting.meeting_id,
scheduled_at: meeting.start_time,
meet_link: meeting.meet_link
});
// 4. Send personalized confirmation
await this.sendDemoConfirmation(leadEmail, leadName, meeting);
return {
status: 'scheduled',
meeting,
message: `Demo scheduled for ${new Date(meeting.start_time).toLocaleString()}`
};
}
async checkConnectionStatus(email) {
const response = await fetch(
`https://api.syncline.run/v1/platform/users/status?email=${email}`,
{
headers: { 'X-API-Key': process.env.SYNCLINE_API_KEY }
}
);
return await response.json();
}
async getOAuthUrl(email) {
const response = await fetch(
`https://api.syncline.run/v1/platform/oauth-url?user_email=${email}`,
{
headers: { 'X-API-Key': process.env.SYNCLINE_API_KEY }
}
);
const { oauth_url } = await response.json();
return oauth_url;
}
async sendConnectionRequest(email, name, oauthUrl) {
await sendEmail({
to: email,
subject: 'Connect your calendar for easy scheduling',
body: `
Hi ${name},
To make scheduling our demo easy, please connect your Google Calendar:
${oauthUrl}
Once connected, we'll automatically find a time that works for both of us!
Best,
Sales Team
`
});
}
async sendDemoConfirmation(email, name, meeting) {
await sendEmail({
to: email,
subject: `Demo Confirmed - ${new Date(meeting.start_time).toLocaleDateString()}`,
body: `
Hi ${name},
Your product demo is confirmed!
When: ${new Date(meeting.start_time).toLocaleString()}
Duration: 30 minutes
Join via Google Meet: ${meeting.meet_link}
We've chosen this time based on your calendar preferences.
Looking forward to showing you the product!
Best,
Sales Team
`
});
}
async logMeetingInCRM(data) {
// Log to your CRM (Salesforce, HubSpot, etc.)
await crm.createActivity({
type: 'meeting',
subject: `Product Demo`,
lead: data.lead_email,
assigned_to: data.sales_rep,
scheduled_at: data.scheduled_at,
meet_link: data.meet_link,
external_id: data.meeting_id
});
}
}
// Usage
const agent = new SalesDemoAgent();
// Webhook from your form
app.post('/api/demo-request', async (req, res) => {
const { email, name, notes } = req.body;
const salesRep = assignSalesRep(); // Your logic
const result = await agent.scheduleDemo(email, name, salesRep, notes);
res.json(result);
});
Best Practices
1. Handle Connection Gracefully
Copy
async function ensureConnected(email) {
const status = await checkConnection(email);
if (!status.connected) {
const oauthUrl = await getOAuthUrl(email);
// Send friendly message
return {
connected: false,
message: `To schedule meetings, please connect your calendar:\n${oauthUrl}`,
oauth_url: oauthUrl
};
}
return { connected: true };
}
2. Present Options Clearly
Copy
function formatAvailability(slots) {
return slots.slice(0, 3).map((slot, i) => {
const date = new Date(slot.start_time);
const day = date.toLocaleDateString('en-US', { weekday: 'long' });
const time = date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
});
return `${i + 1}. ${day}, ${time} (${slot.match_quality} match)`;
}).join('\n');
}
3. Learn from Conversations
Copy
async function updatePreferencesFromChat(userEmail, conversationHistory) {
// Extract preferences from natural conversation
const insights = await extractSchedulingInsights(conversationHistory);
if (insights.patterns.length > 0) {
await fetch('https://api.syncline.run/v1/user/preferences', {
method: 'PUT',
headers: {
'X-API-Key': userApiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
scheduling_context: insights.patterns.join('. ')
})
});
}
}