Inspiration
A school rings a parent because their child was marked absent. Somebody answers. They say the child left for school that morning.
That call connected. It was answered. The person gave a clear reply. And at the end of it nobody on the line knew where that child was.
Every dialler I looked at files that call as a success, because somebody picked up. Answered and resolved get treated as the same event. They are not, and the gap between them is where a child stays unaccounted for all morning while the record says the school did its job.
I did not invent that scenario. It is call S-4105 in this repository, placed through CALL-E to a real telephone, and you can hear it in the first 22 seconds of the demo.
What it does
firstbell places a school's morning absence calls through CALL-E, in the family's own language, and returns four plain facts instead of a transcript: does the parent know, why is the child away, when are they back, who has to ring back.
What matters is what happens when it cannot get them. A call has three endings here, not two: resolved, failed, and undetermined. The third is a real outcome with a name, it is owned by a person, and it cannot collapse into either neighbour. A call that came back with no reason stays open and lands on a safeguarding lead's desk.
Three lines it will not cross. No consent record on file, no call. A number the telephone cannot reach goes to a human rather than being filed as "nobody answered", which is what deaf and hard-of-hearing families were quietly being filed as. And if a parent did not know their child was absent, somebody is told at once, whatever the call's ending was.
How we built it
Native CALL-E Python SDK, called at runtime. Not wrapped, not mocked.
The decision I would make again: it runs offline by default. pip install -r requirements.txt, then python -m firstbell --work-file examples/absences.csv. That dials nobody, costs nothing and needs no CALL-E account. There is a local double in calle_double/ that speaks the real API including its error shapes, plus a conformance harness that checks the double against the real thing so it cannot quietly drift.
Live calling needs an explicit flag and an explicit base URL together, and the bearer token is only sent when that URL resolves to the trusted origin, so a mistyped host gets no credential at all.
887 tests. Then I broke 358 of my own gates on purpose, one at a time, to confirm each one fails when it should. A gate nobody has ever seen fail is not a gate.
Challenges we ran into
No sandbox, no dry run, no test key. So I wrote a fake, then had to write a second harness to prove the fake matched reality. Every runnable app in CALL-E's own repository has independently done the same thing.
No way to end a call. No end_call, no max_turns, no maximum duration. On one live call the agent correctly told a parent that staff would ring back, then restarted its opening disclosure from the top to someone who had already answered everything. The parent hung up. I could not stop it.
Twenty real calls to real phones in two languages meant every bug cost money and rang somebody's actual telephone. That changes how carefully you write.
I filed 16 findings back to CALL-E, each naming the file and the line.
Accomplishments that we're proud of
Tamil held up on a real telephone line. Structured extraction worked in a second language on a live call, not in a demo. That is the thing that makes this worth building, and I nearly did not try it because locale reads as cosmetic in the schema.
The morning actually finishes. 500 absences, off real calls averaging 51 seconds: on three concurrent lines the office is still on the phone two hours later. On six it clears in 74.2 minutes, inside the register window.
$0.40 a call on the meter. The same call with a LanguageLine interpreter on it is $3.36. The case was never typing speed. It is a morning handed back to one person, every school day, in languages the office does not speak.
Every screen in the demo is the running product, and the call you hear is the recording CALL-E returned, published unedited.
What we learned
A schema-valid answer is not a correct outcome. That sentence cost me the most.
On call S-3127 the parent asks, in plain words, "Is he missing from the class?" It is right there in the transcript. The structured result for that same call came back parent_confirmed_aware: yes. The alarm was in the conversation and it did not reach the field.
My rule read that one field and closed the record. So a correct-looking answer produced a wrong action, which is the exact failure this whole project argues against, sitting inside the project. firstbell no longer decides anything from a single field, and the safeguarding escalation now runs as its own axis rather than as a consequence of the ending.
I only caught it because I kept receipts for every call and went back through them one at a time. I also learned that a green suite proves nothing by itself: breaking 358 gates deliberately found gates that had never once failed, which meant they were never checking anything.
What's next for firstbell
One school, one term, one number: hours on the phone before and after.
The consent register a district already holds plugs straight in, and it ships as a CALL-E plugin so the next school writes no code. The honest blockers are written down in docs/what-a-pilot-would-look-like.md, including the ones I cannot fix myself.
The next code change is not a feature. CALL-E's webhook taxonomy already names the distinction this whole app is built on, call.result_validation_failed, and I am still polling instead of using it.
Built With
- call-e
- call-e-python-sdk
- css
- html
- javascript
- n8n
- node.js
- puppeteer
- pytest
- python
- vercel
Log in or sign up for Devpost to join the conversation.