Skip to main content

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:
// 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:
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

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

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:
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

// 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

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

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

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('. ')
      })
    });
  }
}