Architecture
A single Go binary. Stdlib only. Every package owns one concern.
The pipeline
Packages
| Package | Owns |
|---|---|
cmd/callhook | Entrypoint, config, graceful shutdown |
cmd/callhookctl | CLI client: fire, batch, sessions, metrics |
internal/api | HTTP: intake (+batch), CALL-E webhook, dashboard, metrics, rate limiting, auth |
internal/events | Event schema + router: event type → blueprint (task composer + result schema) |
internal/business | The Store interface + mock — the surface you implement against your CRM |
internal/callhookclient | CALL-E Developer API client: calls + Goals + webhooks (their OpenAPI spec ships in docs/) |
internal/session | Session registry, audit log, retry/schedule triggers |
internal/outcome | Outcome engine: policy-gated writes, escalation, retry decisions, callback delivery |
internal/retry | Scheduler: redials, calling-window deferrals, scheduled starts |
internal/callwindow | Polite-hours gate: region → timezone, 9:00–20:00 weekdays |
internal/store | Crash-safe JSONL journal persistence |
Design decisions
Read/write separation
Prefetched context is given to the voice agent (customer data is baked into the task — the agent never asks for account numbers). Business writes happen only after the call, from the JSON-Schema-validated structured result — never from raw conversation. Every write is policy-gated and audit-logged.
No extra LLM — deliberately
CALL-E's voice agent is the conversational brain. Task composition is deterministic templates, because a hallucinated amount or date spoken on a phone call is a real failure mode, and orchestration policy ("certain → write, uncertain → escalate") should be auditable code, not model output. callhook doesn't reinvent the voice AI; it makes it pluggable into any business system.
Triggers, not timers
Three deferred-trigger kinds share one scheduler: retry (no answer), window (polite hours), scheduled (caller-requested). Window and scheduled triggers never consume retry attempts — deferring a call is not the customer's fault.
Goals API — the enterprise path
Besides free-text call tasks, CALL-E publishes Goals: reusable, versioned call workflows with typed input and result schemas. callhook supports both. A blueprint can pin GoalID (+ variables mapping) and that event type executes the pinned, schema-validated goal server-side. Free-text keeps callhook zero-setup and fully generic; Goals give enterprises versioned, governed workflows. The client implements ListGoals, CreateGoalRun, GetGoalRun.
What we deliberately did not build
- No telephony. IVR, voicemail, transfer, ~45 countries — CALL-E's job.
- No database. The JSONL journal is dependency-free and honest about hackathon scale; the session store is an interface when Postgres is warranted.
- No framework. stdlib
net/http, stdlib JSON, zero external dependencies — nothing to go stale.
Testing
Unit suites cover the correctness-critical paths: calling-window timezone math (including weekend and evening rollover), journal replay (including torn-line tolerance after a crash), and the outcome policy ladder (promise writes, refusal never redials, blocked numbers never retried, exhaustion stops). CI runs the suite with -race on every push.