AI Knowledge & Logic

Phone Line Routers

Decide in Python, before a call is answered, whether it continues to the phone line or hangs up.

A phone line router runs your Python route_call(context) function when a call reaches a phone line, before the line's own routing. It can let the call continue, or end it. Use it to block known spam numbers, check a caller against your CRM, or close a number on holidays.

The caller hears ringing while the router runs. Changes take effect on the next call, without deploying code.

Create and attach a router

Open Settings -> Voice & SMS -> Phone line routers, create a router, and test it with a sample call. Then attach it to a number from Settings -> Voice & SMS -> Phone numbers. Through MCP or Copilot, use create_phone_line_router, then update_phone_line with its routerId.

Routers belong to the organization. One router can serve many phone lines, also across businesses, and branch on the line that was called. A phone line has at most one router.

BLOCKED = {"+15550000001", "+15550000002"}

def route_call(context):
    call = context["call"]
    if call["caller_phone_number"] in BLOCKED:
        return {"action": "hangup"}
    return None

What the router returns

Return valueWhat happens
None or {"action": "continue"}The call continues with the line's own routing: its time slots, then who answers.
{"action": "hangup"}The call is rejected without being answered. Twilio doesn't charge for it.
{"action": "hangup", "voice_message": "We're closed."}The line answers, says the message in the line's language, and hangs up. The call is billed.

A rejected call creates no conversation. Its only record is the router's execution history.

Call context

context["call"] describes the ringing call. context["business"] holds the name and slug of the called line's business. The usual Python helpers (get_secret, organization_store_*, ...) are globals, as in custom actions. Helpers that work on a conversation do nothing, because there is no conversation yet.

FieldValue
context["call"]["caller_phone_number"]Who is calling, usually in E.164 format
context["call"]["twilio_call_sid"]Twilio's ID of the call. None in a test
context["call"]["is_test"]True when the call is a test
context["call"]["phone_line"]The called line: id, name, address, business_id, language

When a router fails, the call continues

A router never drops a call by accident. The call continues with the line's own routing when the router:

  • raises an error,
  • returns anything other than the values above,
  • takes longer than 5 seconds, or
  • can't run yet because the organization's Python sandbox is starting up.

The sandbox starts the first time the organization runs Python, and again after nothing ran for a while. Starting takes 10 to 15 seconds, longer than a caller can wait, so the router is skipped for that call while the sandbox starts in the background. A test starts it too, so the first real call after testing finds it ready.

Each of these shows up in the execution history as outcome fail_open, with the reason error, invalid_result, timeout or sandbox_starting.

Test a router

Test a router with a sample call: a caller number and the phone line it calls. The test runs the code exactly like a real call, with the same context and deadline, and shows the outcome, the returned value, printed output, any error with its traceback, and how long it took. You can test unsaved code before saving it.

If the sandbox had to start first, the test waits for it and says so. A real call at that moment would have skipped the router.

Tests run real code: requests and writes your code makes are real.

Execution history

Every call through a router and every test is recorded for 90 days: the caller, the phone line, the outcome, the returned value, the duration, and any error with its output. Read it with list_phone_line_router_executions, or the last few runs with get_phone_line_router.

Versions and archiving

Every change is saved as a version. Restore one with restore_phone_line_router_version. A router can't be archived while a phone line uses it; detach it first. Restoring a version also restores an archived router.

On this page