skills/voice-telephony/SKILL.md
Wire outbound (and inbound) AI phone calls into an app, with a switchable telephony provider. Covers three paths, Telnyx-native AI agent, Vapi + Telnyx carrier, and Vapi + Twilio carrier, plus the exact API calls to buy a number, register it, and place an outbound call. Use when a product needs an AI assistant to phone a user (a "call me" feature), live call transcription, and a post-call summary. Triggers on "AI phone call", "outbound call", "vapi telnyx", "voice agent calls a number", "call me feature".
npx skillsauth add RonanCodes/ronan-skills voice-telephonyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Give an app a real phone call: an AI assistant (e.g. "Saar") dials a user's number, talks, and you get live transcription + a summary. There are three viable paths; build a thin provider abstraction so an admin can switch between them.
| Path | Vendors / keys | Outbound call API | Best for |
|---|---|---|---|
| B. Telnyx-native | One (Telnyx KEY...) | POST /v2/texml/ai_calls/{texml_app_id} with {From, To, AIAssistantId} | Fewest moving parts, single vendor |
| A. Vapi + Telnyx carrier | Vapi key + Telnyx key | POST api.vapi.ai/call with {assistantId, phoneNumberId, customer.number} | Best-documented live-transcription webhooks |
| C. Vapi + Twilio carrier | Vapi key + Twilio SID/token | same as A, number imported from Twilio | If Twilio is already wired (e.g. WhatsApp) |
Cost: Vapi orchestration ~$0.05/min; "$0 with BYO key" means $0 markup, not $0 total (all-in ~$0.15-0.36/min with your own LLM/TTS/STT). Telnyx EU outbound is low single-digit cents/min, cheaper than Twilio.
From, enable the target country (NL) as a destination on an Outbound Voice Profile, and call. Swap to a real +31 caller-ID later once paperwork clears.# 1. Buy a US number on Telnyx (instant)
curl "https://api.telnyx.com/v2/available_phone_numbers?filter[country_code]=US&filter[features][]=voice" \
-H "Authorization: Bearer $TELNYX_API_KEY"
curl -X POST https://api.telnyx.com/v2/number_orders -H "Authorization: Bearer $TELNYX_API_KEY" \
-H "Content-Type: application/json" -d '{"phone_numbers":[{"phone_number":"+1XXXXXXXXXX"}]}'
# 2. On Telnyx: an Outbound Voice Profile with the Netherlands enabled as a destination,
# and add Vapi as a Connection (Connections & Applications). (Dashboard step.)
# 3. Store the Telnyx key as a Vapi credential, then import the number:
curl -X POST https://api.vapi.ai/phone-number -H "Authorization: Bearer $VAPI_PRIVATE_KEY" \
-H "Content-Type: application/json" \
-d '{"provider":"telnyx","number":"+1XXXXXXXXXX","credentialId":"<telnyx-credential-uuid>","assistantId":"<assistant>","name":"Saar line"}'
# 4. Place the call (assistantOverrides + schedulePlan optional)
curl -X POST https://api.vapi.ai/call -H "Authorization: Bearer $VAPI_PRIVATE_KEY" \
-H "Content-Type: application/json" \
-d '{"assistantId":"<assistant>","phoneNumberId":"<pn_id>","customer":{"number":"+31647305600"}}'
Schedule a future call: add "schedulePlan": { "earliestAt": "<iso>" }.
Live call webhooks (same serverUrl as web calls): transcript (live chunks), status-update (queued/ringing/in-progress/ended), end-of-call-report (summary + full transcript + recording URL). Drive the live-transcription panel + post-call summary off these.
SIP gotchas (only if using the BYO SIP-trunk path, provider: byo-phone-number): use Telnyx signaling IP addresses in gateways (FQDN sip.telnyx.com returns 400), and whitelist all Telnyx signaling IPs (else 401 on inbound).
curl -X POST https://api.telnyx.com/v2/texml/ai_calls/{texml_app_id} \
-H "Authorization: Bearer $TELNYX_API_KEY" -H "Content-Type: application/json" \
-d '{"From":"+1XXXXXXXXXX","To":"+31647305600","AIAssistantId":"assistant-..."}'
Telnyx AI Assistants support Webhook tools, Handoff/Transfer, DTMF, and an MCP server. Weak spot: the real-time transcription webhook contract is less explicitly documented than Vapi's, so verify it in the AI Assistant settings before committing a live-transcript UI to Path B.
Build a calls provider interface so the app picks the path at runtime:
type CallProvider = "telnyx-native" | "vapi-telnyx" | "vapi-twilio";
async function placeCall(provider: CallProvider, to: string, opts): Promise<{id}> { /* dispatch */ }
Store the chosen provider in an admin setting; expose a small admin panel with the three options + a "Run & Talk" test. The assistant/tools layer stays the same across providers; only the carrier + call-placement call differs.
Once you own a number, it can do SMS too — bill it as a second channel on the same line and route by destination country (mirror the call routing: +31 → your Telnyx number, else Twilio/other).
# 1. A messaging profile is REQUIRED and MUST list whitelisted destinations
# (country codes) or creation 400s ("missing whitelisted destinations").
MPID=$(curl -s -X POST https://api.telnyx.com/v2/messaging_profiles \
-H "Authorization: Bearer $TELNYX_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"<app> NL","enabled":true,"whitelisted_destinations":["NL"]}' | jq -r .data.id)
# 2. Assign it to the number via the MESSAGING sub-resource (PATCH on the number
# root with messaging_profile_id silently no-ops — use /messaging):
curl -X PATCH "https://api.telnyx.com/v2/phone_numbers/<NUM_ID>/messaging" \
-H "Authorization: Bearer $TELNYX_API_KEY" -H "Content-Type: application/json" \
-d "{\"messaging_profile_id\":\"$MPID\"}"
# 3. Send: POST /v2/messages { from: <your E.164>, to, text }. data.id is the sid.
App side: a sendSmsTelnyx(to, body) (server-only fetch to /v2/messages) + route in your generic sendSMS: if countryFromPhone(to)==="NL" && telnyxConfigured() → Telnyx, else Twilio. Env: TELNYX_API_KEY, TELNYX_SMS_FROM=<E.164>.
The Vapi+Telnyx import is genuinely two API calls, no manual SIP trunk:
# a. register the Telnyx key as a Vapi credential
CRED=$(curl -s -X POST https://api.vapi.ai/credential -H "Authorization: Bearer $VAPI_PRIVATE_KEY" \
-H "Content-Type: application/json" -d "{\"provider\":\"telnyx\",\"apiKey\":\"$TELNYX_API_KEY\"}" | jq -r .id)
# b. import the number — Vapi AUTO-CREATES the Telnyx voice connection on import
# (the number's "Required for calls" warning clears itself; no dashboard step)
curl -s -X POST https://api.vapi.ai/phone-number -H "Authorization: Bearer $VAPI_PRIVATE_KEY" \
-H "Content-Type: application/json" \
-d "{\"provider\":\"telnyx\",\"number\":\"+31...\",\"credentialId\":\"$CRED\",\"name\":\"<app> NL\"}"
# → returns { id: <phoneNumberId>, status: "active" } → set VAPI_PHONE_NUMBER_ID_NL
Gotchas confirmed: (1) messaging-profile creation needs whitelisted_destinations; (2) assign the profile via /phone_numbers/{id}/messaging, not the number root; (3) the Vapi Telnyx import provisions the voice connection for you — don't hand-build a SIP trunk for this path; (4) route SMS + calls by destination country off one client-safe helper so adding a market is one edit.
transcript / end-of-call-report (Path A) or the verified Telnyx event (Path B).Vapi: docs.vapi.ai/api-reference/calls/create, /calls/outbound-calling, /free-telephony, /telnyx, /advanced/sip/telnyx, /advanced/sip/twilio, /server-url/events. Telnyx: developers.telnyx.com/docs/numbers/phone-numbers/buy-phone-number, /docs/inference/ai-assistants, support.telnyx.com NL DID requirements. Pricing: vapi.ai/pricing.
testing
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".