Spin it up

One command starts everything: Postgres, n8n, the public tunnel, the workflow, Layla's tools and webhook, and the report's live connection.

Demo controls

One click puts Layla in out-of-hours mode (same agent, same number) and opens a new Demo shift, so calls during the meeting show up as tonight's shift on the Overnight page. The override lives in Postgres (app_settings), so tools, SLAs, the website greeting and the report all follow it. db/reset_demo.sh turns it off.

Reading the current mode…

Calls VP_DEMO_PHONE. Only inside the UAE calling window (Mon–Fri 09:00–18:00) unless the override is on.

Start

In VS Code, open the terminal (Ctrl + `). It opens in the project folder.

./start.sh

This takes about 30 seconds and opens the customer page and this report. Add --no-open to skip opening them, or --redeploy to push prompt or tool changes to Layla.

./stop.sh          # stops the tunnel and Postgres (data is kept)
./stop.sh --all    # also stops the n8n container

What start.sh does

StepWhat happensWhere
1 · DockerOpens Docker Desktop if it isn't running
2 · PostgresStarts voicepilot-postgres and applies db/schema.sql and db/seed.sql (safe to re-run)localhost:5433
3 · n8nStarts your local-n8n container and waits until it is healthylocalhost:5678
4 · TunnelReuses the Cloudflare quick tunnel if it still works, otherwise starts a new one. ElevenLabs needs a public HTTPS URL.*.trycloudflare.com
5 · WorkflowPushes n8n/voicepilot-construction.json with your key and the Postgres credential, then activates itn8n
6 · LaylaIf the tunnel URL changed: updates the 10 tools, the post-call webhook, prompts, guardrails and voiceElevenLabs
7 · ReportWrites dashboard/config.js so the Report and Live data pages connect on their ownthis page
8 · Smoke testCalls the KPI feed and a tool through the tunnel, the way ElevenLabs will

First time on a new machine

  1. Install Docker Desktop, Python 3 and cloudflared (brew install cloudflared).
  2. Run n8n in Docker and create an API key in n8n under Settings → n8n API.
  3. cp .env.example .env, then fill in ELEVENLABS_API_KEY, N8N_API_KEY, VOICEPILOT_KEY and your n8n container name.
  4. Create db/.env with POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB=voicepilot and VOICEPILOT_APP_PASSWORD.
  5. Run ./start.sh.

Both .env files, .run/ and config.js are gitignored. The scripts never print keys.

Prompt for VS Code (Claude Code)

Paste this into Claude Code in VS Code and it will bring the stack up and fix anything that fails.

Start the VoicePilot demo in this repo. Run ./start.sh --no-open and fix anything that fails (Docker Desktop not running, the n8n container stopped, Postgres not healthy, the tunnel not reachable). Then confirm, with evidence:
1. voicepilot-postgres is healthy on localhost:5433 and the voicepilot schema has customers and kb_articles.
2. The n8n workflow "VoicePilot AI | Construction customer care" is active and the tool, post-call, kpi and data webhooks answer locally.
3. The tunnel URL in .run/tunnel_url reaches n8n, and Layla's ElevenLabs tools and post-call webhook point at it. If they don't, run ./start.sh --redeploy.
4. A test call to the search_help_center tool through the tunnel creates a row in voicepilot.tool_events.
Finally open web/index.html and dashboard/live.html and give me the URLs. Never print or commit anything from .env or db/.env.

Phone number for testing

Layla answers calls through a Twilio number (or a SIP trunk) connected in ElevenLabs. Caller ID lets her verify a customer without asking for their name.

Option A · Use a Twilio number you already have

Assign it to Layla. This removes it from whichever agent it is on now.

set -a; . ./.env; set +a
python3 elevenlabs/assign_phone.py --list
python3 elevenlabs/assign_phone.py +1XXXXXXXXXX

Option B · Get a new Twilio number

Buy a voice-capable number in Twilio. Then in ElevenLabs go to Phone numbers → Import → Twilio, give it the number, Account SID and Auth Token, and assign it to Layla. Or import it with assign_phone.py --import.

A UAE (+971) number for a real client needs a local SIP trunk and TDRA approval. A US or UK Twilio number is fine for testing.

Make caller ID verification work for you

Put your own mobile on a demo customer, then call the number:

docker exec voicepilot-postgres psql -U voicepilot_owner -d voicepilot -c "update voicepilot.customers set phone = '+9715XXXXXXXX' where customer_id = 'C-1002'"

Re-running db/setup.sh or ./start.sh resets the demo phone numbers.

Call doesn't connect?

Host the website for free

Only the Crestline website (site/) goes public. The report, live data and demo guide stay on your machine because they show customer data.

  1. ngrok (permanent address for n8n): sign up free at dashboard.ngrok.com. Copy Your Authtoken, then under Domains claim your free static domain (for example crestline-demo.ngrok-free.app).
  2. Cloudflare Pages (hosts the site): sign up free at dash.cloudflare.com. Copy your Account ID (Workers & Pages overview, right side). Then go to My Profile → API Tokens → Create Token → Custom token, with permission Account → Cloudflare Pages → Edit.
  3. Add to .env:
    NGROK_AUTHTOKEN=...
    NGROK_DOMAIN=your-name.ngrok-free.app
    CLOUDFLARE_API_TOKEN=...
    CLOUDFLARE_ACCOUNT_ID=...
    VP_PAGES_PROJECT=crestline-demo
  4. Run ./start.sh --redeploy once. Layla's tools, the post-call webhook and the website now use the permanent address, and the site is live at https://crestline-demo.pages.dev. Every later ./start.sh republishes it.

The site and Layla only work while this Mac runs n8n and the tunnel. For always-on, move n8n and Postgres to a small cloud server.

WhatsApp and email confirmations

Every booking, reschedule, cancellation, ticket and callback writes a WhatsApp message and an email, in the customer's language, to voicepilot.notifications (see Live data → Outbox). n8n sends them right after the tool call. Until a provider is connected they are marked simulated, so nothing reaches the fake demo numbers.

OptionGood forWhat you need.env
Twilio WhatsApp sandboxFastest demo: working in about 10 minutesTwilio account (free trial works). In WhatsApp, send the sandbox join code to +1 415 523 8886 from your phone.VP_WHATSAPP_PROVIDER=twilio
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN
SMS from your Twilio numberPlain text messages, no WhatsApp setupYour existing Twilio number. Trial accounts only text verified numbers. UAE networks often filter SMS from US numbers; WhatsApp is more reliable there.VP_WHATSAPP_PROVIDER=sms
TWILIO_SMS_FROM=+14244010688
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN
Meta WhatsApp Cloud APINo Twilio; closest to productionMeta developer app with WhatsApp: a free test number, a token, and your phone added as a test recipient. Messages outside a 24-hour chat window need approved templates.VP_WHATSAPP_PROVIDER=meta
META_WA_TOKEN, META_WA_PHONE_ID
Resend (email)Email in 5 minutesFree Resend account and API key. Until you verify a domain it can only send to your own address.VP_EMAIL_PROVIDER=resend
RESEND_API_KEY

During demos set VP_DEMO_PHONE and VP_DEMO_EMAIL to your own number and email so every confirmation comes to you. Then run ./start.sh. For a real client, WhatsApp needs a verified WhatsApp Business account (through Twilio, Meta or 360dialog) with approved message templates.

Where things are

WhatWhere
n8n workflow, Executions tab = every webhookhttp://localhost:5678
Tool webhook/webhook/6b375bba-…/voicepilot/tool/:name
Post-call webhook/webhook/voicepilot/post-call?key=…
KPI feed (Report page)/webhook/voicepilot/kpi?key=…&days=30
Live data feed (Live data page)/webhook/voicepilot/data?key=…
Postgres (TablePlus, DBeaver, psql)localhost:5433 · db voicepilot · owner credentials in db/.env
Useful viewsv_transcripts, v_open_cases, v_daily, v_intents, v_tool_daily
ElevenLabs agentelevenlabs.io/app/agents → "VoicePilot | Layla"

Troubleshooting

SymptomFix
Layla says the system is unavailablePostgres is down. Run ./start.sh.
Layla's tools fail or time outThe tunnel changed or stopped. Run ./start.sh; it re-points Layla automatically.
Conversations never show upCheck the post-call webhook in ElevenLabs → Settings → Webhooks (it must be enabled), then n8n → Executions.
Report says "could not load"n8n isn't running, or config.js is missing. Run ./start.sh.
"lookup_locked" in Live dataThree wrong name or unit attempts. Start a new conversation.
Chat ends right awayA guardrail fired, probably prompt injection. Start a new chat.