A 24×7 AI calling desk built on CALL-E.
Feed in contacts and a goal. CallFlow AI dials, holds real conversations, extracts typed results, and escalates only what needs a person.
Built for the CALL-E: Your Code Is Calling hackathon.
Outbound calling costs enormous manual effort and buys you no visibility.
In a normal call log, a delighted customer and a furious one look identical —
both just say completed. Teams burn hours dialing, repeating the same five
questions, and typing notes into a CRM afterwards. You find out a call went
badly when someone escalates, which is far too late.
CallFlow AI turns every conversation into typed data the moment it ends, so the work that needs a human is visible immediately and the rest closes itself.
| Campaigns, not scripts | Write a goal in plain English. CALL-E improvises the conversation and adapts when people go off-script. |
| Typed results | Every call returns schema-validated JSON via CALL-E's native result_schema. No transcript scraping. |
| Sentiment triage | Frustration and opt-outs escalate to a person. Bad timing is queued for a polite retry. Clean calls auto-close. |
| Build your own | Create unlimited campaigns from the dashboard with custom extraction fields. |
| Safe by default | Dry run is on until you turn it off. An allowlist and per-run ceiling stop accidental calls. |
Next.js dashboard FastAPI orchestrator CALL-E
----------------- -- HTTP -> -------------------- -- SDK -> ------
campaigns safety gate dials
contact table E.164 validation speaks
live results allowlist adapts
rate limit listens
call ceiling
|
v
result_schema -- typed JSON back
|
v
triage
|
+---------------------+---------------------+
| | |
v v v
auto-close retry later needs human
CALL-E is the calling agent. It owns the phone number, dialer, speech recognition, conversational model, turn-taking, voicemail detection, and IVR handling. CallFlow AI is the operations layer around it.
Backend — Python 3.11+
python -m venv .venv
.venv/Scripts/activate # Windows · source .venv/bin/activate on Unix
pip install -e .
cp .env.example .env # add your CALLE_API_KEY
python run_api.py # → http://127.0.0.1:8000Frontend — Node 20+
cd web
npm install
npm run dev # → http://localhost:3000Get an API key at dashboard.heycall-e.com. New accounts include free calls.
The hosted dashboard places real phone calls. Enter your own number, pick a campaign, switch to Live mode, and answer — the agent greets you by name and holds an actual conversation. When you hang up, the transcript, sentiment, and extracted fields appear in the results table.
Because the demo runs on a shared CALL-E account, live calls are rate limited:
| Limit | Value |
|---|---|
| Per visitor | 2 live calls per hour |
| Shared daily budget | 20 live calls |
| Dry run | Unlimited, always available |
Only call a number you own or have permission to call.
The free tier sleeps after inactivity, so the first load can take 30–60 seconds. The dashboard shows "Waking the backend…" while it starts.
CallFlow AI places real phone calls. Every guard below fails closed.
| Guard | Behaviour |
|---|---|
CALLFLOW_DRY_RUN |
Defaults to true. Renders and validates everything; dials nothing. |
CALLFLOW_ALLOWLIST |
When non-empty, only these E.164 numbers can be dialed. |
CALLFLOW_MAX_CALLS_PER_RUN |
Hard ceiling per process run. Protects your credit balance. |
CALLFLOW_RATE_LIMIT_CALLS |
Live calls allowed per IP per window (default 2/hour). |
CALLFLOW_DAILY_BUDGET |
Shared daily live-call ceiling across all visitors. |
| E.164 validation | Malformed numbers are rejected before reaching CALL-E. |
The allowlist and the rate limiter solve different problems. Use the allowlist for private development — it makes dialing anyone but yourself impossible. Use the rate limiter for a public deployment, where visitors legitimately need to call their own numbers but no one should be able to drain the account or dial a stranger repeatedly.
Set CALLFLOW_OWNER_KEY and send it as an X-CallFlow-Owner-Key header to
lift the limits for your own testing.
Phone numbers are masked (+15******100) in every log, API response, and UI
surface. All sample data uses reserved fictional numbers (+1 555 0100–0199)
that cannot connect to a real person.
While developing, keep CALLFLOW_DRY_RUN=true and set an allowlist to your
own number.
Dry run exercises the entire pipeline — goal templating, contact validation, the safety gate, and the result shape — without placing a call or spending a credit.
curl -X POST http://127.0.0.1:8000/api/runs \
-H 'Content-Type: application/json' \
-d '{"campaign_id":"travel-discovery","dry_run":true,
"contacts":[{"name":"Aditi","phone":"+15555550100",
"context":{"enquiry_note":"Bali in December"}}]}'A campaign is a goal template plus a result schema:
Campaign(
id="travel-discovery",
name="Travel enquiry follow-up",
goal_template=(
"You are CallFlow AI, a friendly travel consultant calling {name} "
"back about their holiday enquiry.\n\n"
"Open by greeting them by name and confirming this is a good time. "
"If it is not, apologise, ask when to call back, and end politely.\n\n"
"Known context: {enquiry_note}\n\n"
"If they sound annoyed, do not push. Offer a human colleague instead."
),
region="IN",
language="en",
)CALL-E returns this, validated against the schema:
{
"outcome": "interested",
"sentiment": "positive",
"frustration_signals": false,
"destination": "Dubai",
"travel_date": "2026-12-18",
"party_size": 4,
"summary": "Wants a 4-person Dubai package in mid-December."
}Triage reads those typed fields — never prose — to decide the disposition.
| Signal | Disposition |
|---|---|
| Asked never to be called again | Needs human — suppress and log |
| Explicitly asked for a person | Needs human |
| Frustration detected | Needs human |
| Negative tone, no frustration | Retry later — a bad time isn't a bad mood |
| Busy / no answer / voicemail | Retry later |
| Completed, no escalation signals | Auto-closed |
| Endpoint | Purpose |
|---|---|
GET /api/health |
Backend status and active safety settings |
GET /api/campaigns |
List campaigns |
POST /api/campaigns |
Create a campaign with custom extraction fields |
DELETE /api/campaigns/{id} |
Remove a user-created campaign |
POST /api/preview |
Render goals without touching CALL-E |
POST /api/runs |
Start a run (background) |
GET /api/runs/{id} |
Poll status, outcomes, and stats |
callflow/ Python backend
├── api.py FastAPI surface
├── calle_client.py CALL-E SDK wrapper
├── orchestrator.py gate → dial → poll → triage
├── safety.py E.164, masking, dial gate
├── campaigns.py built-in + user campaigns
├── schemas.py result_schema definitions
├── triage.py disposition logic
├── models.py domain models
└── store.py in-memory run store
web/ Next.js 16 dashboard
├── app/ landing page + dashboard
└── lib/ API client, CSV parsing
tests/ 56 tests
pytest -q # 71 passedCoverage focuses on what can cause harm: the safety gate fails closed, masking never leaks more than half a number, dry run provably never touches the gateway, and triage precedence is correct.
language is not a valid CALL-E recipient field — it's locale. The API
rejects the former with a 422 extra_forbidden, which is only discoverable from
the error payload.
CALL-E validates task substance. A thin goal like "Call and ask about their trip" is rejected with call_not_ready. The goal must state what to say, ask,
and do on success or failure — the dashboard enforces a 40-character minimum.
The goal field is the entire product. A one-line task produced a confusing robocall; the same platform with a well-written goal produced a consultant. That difference is what CallFlow AI's campaign templates exist to manage.
Working — campaigns (built-in and user-created), safety gate, CALL-E integration, typed extraction, sentiment triage, CSV import, dashboard with live polling, dry-run mode.
Right now the call ends with the agent saying a consultant will follow up. The natural next step is to send the contact everything that was agreed, in writing, on WhatsApp — so they have a record and the team has a receipt.
The structured result already contains everything the message needs:
{
"destination": "Dubai",
"travel_date": "2026-12-18",
"party_size": 4,
"service_interest": "package",
"ready_for_quote": true
}A post-call step would render that into a template message and send it through the WhatsApp Cloud API, keyed off the contact who just spoke.
Two constraints shape the design:
- Consent must come from the call. The agent has to ask, and the answer has to land in the schema as a typed field, before anything is sent. A silent message to someone who didn't agree is worse than no message.
- Business-initiated messages need pre-approved templates. WhatsApp only allows free-form replies inside a 24-hour window the user opened. Outside it, messages must use an approved template and are billed per conversation — so this is a paid, approval-gated integration, not a free bolt-on.
The config hooks (WHATSAPP_TOKEN, WHATSAPP_PHONE_NUMBER_ID) already exist
and read as empty strings when unset.
| Booking tools | Let a campaign call MCP tools — flights, hotels, tours — driven by the extracted outcome, so a confirmed intent books itself. |
| Scheduled campaigns | Recurring runs with time-zone-aware calling windows, so nobody is dialed at 3am local time. |
| Persistent storage | Runs are in-memory today. Postgres would give history, per-campaign analytics, and sentiment trends over time. |
| Inbound calls | Handle calls coming in, not just going out. |
| CRM write-back | Push outcomes to HubSpot or Salesforce so results land where the team already works. |
| Retry orchestration | Act on the retry disposition automatically instead of only surfacing it. |