Inspiration

Every phone workflow assumes that somewhere out there is a desk that owns your request. Often there isn't. The retailer says the damage is the carrier's claim; the carrier says the box was packed by the shipper, so it's the retailer's claim. The question is never answered — and you were never actually refused, so you have nothing to escalate and nothing to attach to a complaint.

What it does

Runaround makes the unit of work the referral edge rather than the call: one desk, the desk it named, and the sentence that named it. It places one CALL-E call at a time, reads back a structured result asking whether this desk owns the request and, if not, who does and in whose words, and follows the chain until it reaches an owner or closes on itself. A detected loop is a finding, not a failure: it produces a dated evidence pack quoting both organizations naming each other, with phone numbers masked, ready to attach to a complaint.

How I built it

A Python app against the CALL-E Developer API, standard library only.

  • POST /v1/calls with a seven-field result_schema and an Idempotency-Key derived from the authorization to place the hop — case id, hop index, destination — rather than from the attempt, so a retried timeout returns the original call instead of ringing a person twice.
  • GET /v1/calls/{call_id} polled to a terminal state. A poll ceiling that is reached raises rather than returns: the call may still be running, and an unknown outcome must be reconciled by a person before the case dials anyone again.
  • All chain logic is pure and network-free, so 61 tests exercise it — plus the API adapter through an injected transport — without opening a socket.

Two rules carry the design. A referral advances the chain only when the recipient's own words come with it; a number returned without a quote is an extraction artifact and is refused. And a number spoken on a call is not permission to dial it — new destinations wait for a person.

Challenges I ran into

Loop detection is only as good as its notion of identity. The same desk read back as "+1 (555) 010-0" and "+15550100" would be two desks to a string comparison, and the chain would circle forever while every check stayed green. Normalizing to E.164 in exactly one place fixed it, and a fixture deliberately returns the punctuated form so the test proves it.

The harder call was what to do when two different numbers answer for the same organization name. That is genuinely ambiguous — a real transfer inward looks exactly like being sent in a circle — so the code does not pretend to see the difference. It reports loop_suspected and lets a person decide.

Accomplishments that I'm proud of

Every state the chain can end in says something a person can act on, and none of them is "error". A failed call is unreachable, never silently read as "no referral". A desk that owns the request but did not answer today is owner_without_answer, not a resolution.

What I learned

The dangerous failure in a referral workflow is not a wrong answer, it is a plausible one. A referral_target_phone with no referral_quote has two indistinguishable causes: the model dropped the sentence, or the model invented the number. Confidence in the first is not a licence to act on the second.

What's next

Multi-branch chains, where a desk names two possible owners and both are worth a call, and a webhook path so long chains do not depend on a process staying alive.

Built With

Share this project:

Updates

Submission history