Last Updated: March 25, 2026
Status: Twilio account upgraded, ready for Canadian number provisioning and SIP trunk setup
Riley is an AI voice agent that calls Indeed leads who applied to become cleaning subcontractors for One Janitorial. She:
AC6bb67368cd733a0646584398b1d9f0793a4f23070de0e2a356779bd8eb97e0e5sk_e584043933189e7f38408e2613acf82c33048c4e4332d3f5agent_5601kmj3br9meb1vf62wy1d6krdhpat-na1-a6c8782e-f339-4209-b602-f0cc1fb1913briley_*)/home/ubuntu/.openclaw/workspace/riley-ai-agent-documentation.mdWhere to go: https://console.twilio.com → Phone Numbers → Buy a number
What to do:
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"
Where to go: https://elevenlabs.io/app/agents/phone-numbers
What to do:
Basic Configuration:
Inbound Configuration:
Outbound Configuration:
sip.twilio.com (hostname only, no sip: prefix)Optional Settings:
Result: ElevenLabs will now accept calls on sip:+1XXXXXXXXXX@sip.rtc.elevenlabs.io:5060;transport=tcp
Where to go: https://console.twilio.com → Phone Numbers → Manage → Active numbers → Click your Canadian number
What to do:
Voice Configuration:
https://YOUR-WEBHOOK-SERVER.com/twilio/voice (you need to build this — see Step 4)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"
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.
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:
indeed_lead = YES (or whatever identifies Indeed leads)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:
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"
}
}'
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"
}]
}]
}'
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}]
}]
}'
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:
riley_victor_meeting_booked = NO, email includes booking linkTrigger: 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:
riley_victor_meeting_booked = YESriley_victor_meeting_date = [DATETIME]Trigger: riley_screening_status = "QUALIFIED"
Actions:
trainual_partner_access = "YES" (auto-delivers training)riley_victor_meeting_booked)Email comes from: Victor Ndubuisi (victor@onejan.com)
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) |
Full prompt location: /home/ubuntu/.openclaw/workspace/riley-ai-agent-documentation.md
Summary:
riley_* properties updated?Total: ~$125-130/month
Call Flow:
Tech Stack:
trainual_partner_access property)/home/ubuntu/.openclaw/workspace/riley-ai-agent-documentation.md+1XXXXXXXXXXStatus: Ready for Twilio + ElevenLabs SIP connection. Everything else is built and waiting.
Point of Contact: Peter Boland (peter@onejan.com / Telegram @nick_holding)
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:
Key rules:
Resources:
Start by asking me: 'Which step are you on? I'll walk you through it.'"
End of handoff document.