One Janitorial

Riley AI — Complete Handoff to Farouk

Status: Twilio upgraded • Ready for Canadian number + SIP trunk setup • Created: March 25, 2026
npm warn exec The following package was not found and will be installed: marked@17.0.5

Riley AI — Complete Handoff Package for Farouk

Last Updated: March 25, 2026
Status: Twilio account upgraded, ready for Canadian number provisioning and SIP trunk setup


🎯 What Riley Does

Riley is an AI voice agent that calls Indeed leads who applied to become cleaning subcontractors for One Janitorial. She:


🔐 Login Credentials

Twilio

ElevenLabs

HubSpot


📋 What's Already Built

✅ ElevenLabs Setup (COMPLETE)

✅ HubSpot Setup (COMPLETE)

✅ Documentation (COMPLETE)

⚠️ Twilio Setup (PARTIAL)

❌ HubSpot Webhooks (NOT STARTED)


🛠️ Farouk's Execution Checklist

Step 1: Provision Canadian Phone Number in Twilio

Where to go: https://console.twilio.com → Phone Numbers → Buy a number

What to do:

  1. Click "Buy a number"
  2. Select Canada as country
  3. Filter by:
    • Voice capability required
    • Pick area code (604 Vancouver, 416/647 Toronto, 403/587 Calgary, etc.)
  4. Buy the number
  5. Save the new phone number (format: +1XXXXXXXXXX)

API Method (Alternative):

curl -X POST "https://api.twilio.com/2010-04-01/Accounts/AC6bb67368cd733a0646584398b1d9f079/IncomingPhoneNumbers.json" \
  -u "AC6bb67368cd733a0646584398b1d9f079:3a4f23070de0e2a356779bd8eb97e0e5" \
  -d "PhoneNumber=+1CANADIANUMBER" \
  -d "FriendlyName=Riley AI - One Janitorial"

Step 2: Import SIP Trunk in ElevenLabs

Where to go: https://elevenlabs.io/app/agents/phone-numbers

What to do:

  1. Click "Import a phone number from SIP trunk"
  2. Fill in the form:

Basic Configuration:

Inbound Configuration:

Outbound Configuration:

Optional Settings:

  1. Click "Import"
  2. Assign Riley agent to the phone number (dropdown should show "Riley")

Result: ElevenLabs will now accept calls on sip:+1XXXXXXXXXX@sip.rtc.elevenlabs.io:5060;transport=tcp


Step 3: Configure Twilio Phone Number to Route to ElevenLabs

Where to go: https://console.twilio.com → Phone Numbers → Manage → Active numbers → Click your Canadian number

What to do:

Voice Configuration:

  1. Under "A call comes in" section:
    • Select "Webhook"
    • URL: https://YOUR-WEBHOOK-SERVER.com/twilio/voice (you need to build this — see Step 4)
    • Method: HTTP POST

Alternative: Configure via API

curl -X POST "https://api.twilio.com/2010-04-01/Accounts/AC6bb67368cd733a0646584398b1d9f079/IncomingPhoneNumbers/PNXXXXXXXXXXXXXXXXXX.json" \
  -u "AC6bb67368cd733a0646584398b1d9f079:3a4f23070de0e2a356779bd8eb97e0e5" \
  -d "VoiceUrl=https://YOUR-WEBHOOK-SERVER.com/twilio/voice" \
  -d "VoiceMethod=POST"

Step 4: Build TwiML Webhook (Routes Calls to ElevenLabs)

What this does: When Twilio receives an inbound call, it asks your webhook "what should I do?" Your webhook responds with TwiML XML that tells Twilio to forward the call to ElevenLabs via SIP.

Endpoint: POST /twilio/voice

TwiML Response (XML):

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Dial>
        <Sip>sip:+1XXXXXXXXXX@sip.rtc.elevenlabs.io:5060;transport=tcp</Sip>
    </Dial>
</Response>

Replace +1XXXXXXXXXX with your Canadian phone number (must match ElevenLabs import exactly, including the +).

Example Node.js Webhook (Express):

const express = require('express');
const app = express();

app.post('/twilio/voice', (req, res) => {
  const canadianNumber = '+1XXXXXXXXXX'; // Replace with actual number
  
  const twiml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Dial>
        <Sip>sip:${canadianNumber}@sip.rtc.elevenlabs.io:5060;transport=tcp</Sip>
    </Dial>
</Response>`;

  res.type('text/xml');
  res.send(twiml);
});

app.listen(3000, () => {
  console.log('Twilio webhook running on port 3000');
});

Deployment: Host this webhook on a public server (DigitalOcean, Heroku, Vercel, etc.) with HTTPS enabled.


Step 5: Trigger Outbound Calls from HubSpot

Goal: When a new Indeed lead is added to HubSpot, trigger Riley to call them.

Method: Use ElevenLabs Outbound Call API

API Endpoint:

POST https://api.elevenlabs.io/v1/convai/agents/phone-calls/make-outbound

Request Body:

{
  "agent_id": "agent_5601kmj3br9meb1vf62wy1d6krdh",
  "agent_phone_number_id": "PHONE_NUMBER_ID_FROM_ELEVENLABS",
  "to_number": "+1LEADPHONENUMBER",
  "conversation_initiation_client_data": {
    "first_name": "John",
    "last_name": "Doe",
    "contact_id": "12345"
  }
}

How to get agent_phone_number_id:

curl -X GET "https://api.elevenlabs.io/v1/convai/phone-numbers" \
  -H "xi-api-key: sk_e584043933189e7f38408e2613acf82c33048c4e4332d3f5"

Look for the phone number you imported via SIP trunk, grab its ID.

HubSpot Workflow Trigger:

  1. Trigger: Contact is created (or updated with specific property)
  2. Filter: indeed_lead = YES (or whatever identifies Indeed leads)
  3. Action: Webhook to your server → Your server calls ElevenLabs API → Riley calls the lead

Step 6: Build 5 HubSpot Webhooks (Post-Call Data Sync)

After Riley completes a call, you need to sync the data back to HubSpot. ElevenLabs provides post-call webhooks.

Where to configure: https://elevenlabs.io/app/agents → Click Riley → Settings → Webhooks

5 Webhooks You Need:

Webhook 1: Update Contact Properties

Trigger: After call ends
Purpose: Write all 20 riley_* properties to HubSpot

ElevenLabs Webhook Payload (example):

{
  "call_id": "abc123",
  "agent_id": "agent_5601kmj3br9meb1vf62wy1d6krdh",
  "contact_phone": "+14031234567",
  "transcript": "...",
  "metadata": {
    "riley_screening_status": "QUALIFIED",
    "riley_has_vehicle": "Yes",
    "riley_cities_serviced": "Calgary, Airdrie"
  }
}

Your Webhook Action:

curl -X PATCH "https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}" \
  -H "Authorization: Bearer pat-na1-a6c8782e-f339-4209-b602-f0cc1fb1913b" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "riley_screening_status": "QUALIFIED",
      "riley_has_vehicle": "Yes",
      "riley_cities_serviced": "Calgary, Airdrie"
    }
  }'

Webhook 2: Search Contact by Phone

Trigger: At call start
Purpose: Find the HubSpot contact ID by phone number

API Call:

curl -X POST "https://api.hubapi.com/crm/v3/objects/contacts/search" \
  -H "Authorization: Bearer pat-na1-a6c8782e-f339-4209-b602-f0cc1fb1913b" \
  -H "Content-Type: application/json" \
  -d '{
    "filterGroups": [{
      "filters": [{
        "propertyName": "phone",
        "operator": "EQ",
        "value": "+14031234567"
      }]
    }]
  }'

Webhook 3: Log Call Transcript

Trigger: After call ends
Purpose: Create a note on the contact timeline with full transcript

API Call:

curl -X POST "https://api.hubapi.com/crm/v3/objects/notes" \
  -H "Authorization: Bearer pat-na1-a6c8782e-f339-4209-b602-f0cc1fb1913b" \
  -H "Content-Type: application/json" \
  -d '{
    "properties": {
      "hs_timestamp": 1774642800000,
      "hs_note_body": "Riley AI Screening Call Transcript:\n\n[FULL TRANSCRIPT HERE]",
      "hubspot_owner_id": "87738250"
    },
    "associations": [{
      "to": {"id": "CONTACT_ID"},
      "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 202}]
    }]
  }'

Webhook 4: Check Victor's Calendar Availability

Trigger: After qualified call (when Riley collects preferred meeting times)
Purpose: Check if any of the 3 preferred times are free on Victor's calendar

API Call:

curl -X GET "https://api.hubapi.com/calendar/v1/events/search?user_id=87738250&start_timestamp=1774642800000&end_timestamp=1774729200000" \
  -H "Authorization: Bearer pat-na1-a6c8782e-f339-4209-b602-f0cc1fb1913b"

Logic:

Webhook 5: Auto-Book Victor's Calendar

Trigger: After Webhook 4 finds an available slot
Purpose: Create a meeting on Victor's calendar

API Call:

curl -X POST "https://api.hubapi.com/calendar/v1/events" \
  -H "Authorization: Bearer pat-na1-a6c8782e-f339-4209-b602-f0cc1fb1913b" \
  -H "Content-Type: application/json" \
  -d '{
    "eventType": "MEETING",
    "startTime": 1774642800000,
    "endTime": 1774644600000,
    "title": "Alliance Programme Follow-Up - [CONTACT NAME]",
    "description": "Follow-up meeting with BCO subcontractor lead.",
    "internal_attendees": ["87738250"],
    "associations": {
      "contactIds": [CONTACT_ID]
    }
  }'

After booking, set:


📧 Email Workflow (Already Built in HubSpot)

Trigger: riley_screening_status = "QUALIFIED"

Actions:

  1. Set trainual_partner_access = "YES" (auto-delivers training)
  2. Send intake form email from Victor
  3. Include confirmed meeting date OR booking link (depending on riley_victor_meeting_booked)

Email comes from: Victor Ndubuisi (victor@onejan.com)


📊 HubSpot Properties (20 Total)

All properties prefixed with riley_ to avoid conflicts with existing BCO Scout workflow.

Property Name Type Purpose
riley_screening_status Dropdown QUALIFIED / DISQUALIFIED / CALLBACK / PENDING
riley_disqualification_reason Single-line text Why they were disqualified
riley_has_subcontract_experience Dropdown Yes - Currently / Yes - In the Past / No
riley_has_vehicle Dropdown Yes / No
riley_team_composition Dropdown Solo / Family/Friend / Business Partner / Employees/Team
riley_has_equipment Dropdown Yes / No / Partial
riley_has_wcb_insurance Dropdown Yes - Have It / No - But Willing / No - Not Willing
riley_industries_excluded Multi-line text Industries they DON'T want to clean
riley_unavailable_times Multi-line text Times they CAN'T clean
riley_cities_serviced Multi-line text Geographic service areas
riley_best_contact_method Dropdown Phone / Text / Email
riley_meeting_preference_1 Date/Time First preferred meeting time
riley_meeting_preference_2 Date/Time Second preferred meeting time
riley_meeting_preference_3 Date/Time Third preferred meeting time
riley_understands_requirements Dropdown Yes / Has Questions
riley_callback_time Date/Time When to call back if now wasn't good
riley_intake_form_sent Dropdown Yes / No
riley_victor_meeting_booked Dropdown Yes / No
riley_victor_meeting_date Date/Time Confirmed meeting date/time
trainual_partner_access Dropdown Yes / No (EXISTING PROPERTY — triggers training)

🎤 Riley's System Prompt (Full Script in Documentation)

Full prompt location: /home/ubuntu/.openclaw/workspace/riley-ai-agent-documentation.md

Summary:


🧪 Testing Checklist

Phase 1: Basic Call Test

  1. Call the Canadian number from your phone
  2. Verify Riley answers
  3. Verify audio quality (both directions)
  4. Complete a full qualification call
  5. Check ElevenLabs dashboard for call recording + transcript

Phase 2: HubSpot Integration Test

  1. Create a test contact in HubSpot with phone number
  2. Trigger outbound call via ElevenLabs API
  3. Verify call completes
  4. Check HubSpot contact → all 20 riley_* properties updated?
  5. Check contact timeline → transcript logged?
  6. Check Victor's calendar → meeting auto-booked (if slot was available)?
  7. Check email → training + intake form sent from Victor?

Phase 3: End-to-End Test

  1. Upload 1 real Indeed lead to HubSpot
  2. Trigger Riley to call
  3. Monitor full flow:
    • Call connects
    • Riley runs through 12 questions
    • HubSpot properties update
    • Training email sends
    • Victor's meeting books (or booking link included)
    • Transcript logs to contact timeline

Phase 4: Scale

  1. Upload 10 leads
  2. Monitor first 10 calls closely
  3. Review call quality, qualification accuracy, data sync
  4. Adjust system prompt if needed
  5. Scale to full lead list once stable

💰 Monthly Costs

Total: ~$125-130/month


📞 Technical Architecture Summary

Call Flow:

  1. HubSpot workflow triggers outbound call (via ElevenLabs API)
  2. ElevenLabs initiates call via Twilio SIP trunk
  3. Twilio dials lead's phone number
  4. Riley AI runs 12-question qualification script
  5. During call: ElevenLabs captures responses in real-time
  6. After call: Webhooks fire → update HubSpot properties, log transcript, check/book Victor's calendar, send email

Tech Stack:


📚 Key Resources


🚨 Important Notes

CRITICAL: Date Format in HubSpot

Phone Number Format Consistency

Training + Intake Form Are MANDATORY

Victor's Meeting Booking


✅ Next Steps for Farouk

  1. Provision Canadian phone number in Twilio (Step 1)
  2. Import SIP trunk in ElevenLabs (Step 2)
  3. Build TwiML webhook to route calls to ElevenLabs (Step 3 + 4)
  4. Set up outbound call trigger from HubSpot (Step 5)
  5. Build 5 HubSpot webhooks for post-call data sync (Step 6)
  6. Run full end-to-end test (Testing Checklist)
  7. Go live with 10 leads → monitor → adjust → scale

Status: Ready for Twilio + ElevenLabs SIP connection. Everything else is built and waiting.

Point of Contact: Peter Boland (peter@onejan.com / Telegram @nick_holding)


CS Agent Training Brief (For Farouk)

What to tell your CS agent:

"You're helping me complete the Riley AI integration for One Janitorial. Riley is an AI voice agent (ElevenLabs) that calls Indeed leads who applied to become cleaning subcontractors. She runs a 10-minute qualification call, then triggers training + intake form emails and auto-books Victor's calendar.

What's already done: ✅ Riley agent built in ElevenLabs ✅ 20 HubSpot properties created ✅ Email workflow built ✅ Twilio account upgraded

Your tasks:

  1. Help me provision a Canadian phone number in Twilio
  2. Guide me through SIP trunk import in ElevenLabs
  3. Build TwiML webhook (routes calls from Twilio → ElevenLabs)
  4. Build 5 HubSpot webhooks (sync call data back to HubSpot)
  5. Test end-to-end flow
  6. Go live with 10 leads

Key rules:

Resources:

Start by asking me: 'Which step are you on? I'll walk you through it.'"


End of handoff document.