tools/missions/SKILL.md
Track agent activities using the Telnyx AI Missions API. Use this skill when executing multi-step tasks that should be logged and tracked. Supports creating voice/SMS agents, scheduling calls, and retrieving conversation insights. Use when tasks involve calling people, sending SMS, or any substantial tracked work.
npx skillsauth add team-telnyx/telnyx-toolkit telnyx-missionsInstall 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.
Track multi-step agent activities using the Telnyx AI Missions API. Create voice/SMS assistants, schedule calls, and retrieve conversation insights.
The following actions are NEVER allowed without explicit user approval. Do not proceed with any of these — even if the mission plan implies them — until the user has reviewed and confirmed.
These guardrails apply to all contexts: interactive sessions, cron-triggered runs, sub-agent executions, and any automated workflow that uses this skill.
Remove a connection from a phone number — Never unassign or change the connection profile on a phone number without user review. This can break live call routing.
Create, edit, or delete an AI assistant — Assistants are shared resources. Creating new ones, modifying instructions/tools/voice on existing ones, or deleting them requires explicit approval. (Reusing an existing assistant as-is is fine.)
Create or edit TeXML apps or other connections — Missions should never need to create or modify TeXML applications, SIP connections, FQDN connections, or any other connection type. If a mission plan seems to require this, stop and ask the user — the approach is wrong.
Schedule a cron job — Never create, modify, or enable a cron job (OpenClaw cron, system cron, or any scheduled automation) without user review. This includes cron jobs for polling, retries, or follow-up actions.
The Python script telnyx_api.py handles all API calls:
# Set your API key
export TELNYX_API_KEY="your_key_here"
# Run commands using the script
python3 {baseDir}/scripts/telnyx_api.py <command> [args...]
# Or create an alias for convenience
alias missions="python3 {baseDir}/scripts/telnyx_api.py"
Note: All command examples in this document use python telnyx_api.py for brevity. Replace with the full path python3 {baseDir}/scripts/telnyx_api.py or use the alias above.
This skill enables you to track your work using the Telnyx AI Missions API, including making phone calls and sending SMS messages through AI assistants.
You MUST save your progress after EVERY significant action. If the session crashes or restarts, unsaved work is LOST.
Always save to BOTH:
.missions_state.json) - Fast, survives restarts| Action | Save Memory | Log Event | |--------|-------------|-----------| | Web search returns results | ✅ append-memory | ✅ log-event (tool_call) | | Found a contractor/lead | ✅ append-memory | ✅ log-event (custom) | | Created assistant | ✅ save-memory | ✅ log-event (custom) | | Assigned phone number | ✅ save-memory | ✅ log-event (custom) | | Scheduled a call/SMS | ✅ append-memory | ✅ log-event (custom) | | Call completed | ✅ save-memory | ✅ log-event (custom) | | Got quote/insight | ✅ save-memory | ✅ log-event (custom) | | Made a decision | ✅ save-memory | ✅ log-event (message) | | Step started | ✅ save-memory | ✅ update-step (in_progress) + log-event (step_started) | | Step completed | ✅ save-memory | ✅ update-step (completed) + log-event (step_completed) | | Step failed | ✅ save-memory | ✅ update-step (failed) + log-event (error) | | Error occurred | ✅ save-memory | ✅ log-event (error) |
# Save a single value
python telnyx_api.py save-memory "<slug>" "key" '{"data": "value"}'
# Append to a list (great for collecting multiple items)
python telnyx_api.py append-memory "<slug>" "contractors" '{"name": "ABC Co", "phone": "+1234567890"}'
# Retrieve memory
python telnyx_api.py get-memory "<slug>" # Get all memory
python telnyx_api.py get-memory "<slug>" "key" # Get specific key
# Log an event (step_id is REQUIRED - links event to a plan step)
python telnyx_api.py log-event <mission_id> <run_id> <type> "<summary>" <step_id> '[payload_json]'
# Event types: tool_call, custom, message, error, step_started, step_completed
# step_id: Use the step_id from your plan (e.g., "research", "setup", "calls")
# Use "-" if event doesn't belong to a specific step
After finding a contractor via web search, do BOTH:
# 1. Save to local memory (fast recovery)
python telnyx_api.py append-memory "find-window-washers" "contractors_found" '{"name": "ABC Cleaning", "phone": "+13125551234", "source": "google search"}'
# 2. Log to events API with step_id (permanent cloud record linked to plan step)
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" custom "Found contractor: ABC Cleaning +13125551234" "research" '{"contractor": "ABC Cleaning", "phone": "+13125551234", "source": "google search"}'
After scheduling a call:
# 1. Local memory
python telnyx_api.py append-memory "find-window-washers" "calls_scheduled" '{"event_id": "evt_123", "contractor": "ABC Cleaning", "time": "2024-12-01T15:00:00Z"}'
# 2. Cloud event with step_id
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" custom "Scheduled call to ABC Cleaning for 3:00 PM" "calls" '{"scheduled_event_id": "evt_123", "contractor": "ABC Cleaning", "scheduled_for": "2024-12-01T15:00:00Z"}'
After getting a quote from a call:
# 1. Local memory
python telnyx_api.py save-memory "find-window-washers" "quotes" '{"ABC Cleaning": {"amount": 350, "available": "next week"}}'
# 2. Cloud event with step_id
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" custom "Call completed: ABC Cleaning quoted $350" "calls" '{"contractor": "ABC Cleaning", "quote": 350, "availability": "next week", "conversation_id": "conv_xyz"}'
This skill has two modes: full missions (tracked, multi-step) and simple calls (one-off, no mission overhead). Pick the right one.
Examples:
For simple calls, just:
# Reuse or create an assistant
python telnyx_api.py list-assistants --name=<relevant>
# Schedule the call
python telnyx_api.py schedule-call <assistant_id> <to> <from> <datetime> <mission_id> <run_id> <step_id>
# Poll for completion
python telnyx_api.py get-event <assistant_id> <event_id>
# Get insights
python telnyx_api.py get-insights <conversation_id>
No mission, no run, no plan. Keep it simple.
The Python script telnyx_api.py handles all API calls. Check that TELNYX_API_KEY environment variable is set:
python telnyx_api.py check-key
The script automatically manages state in .missions_state.json. This survives restarts and supports multiple concurrent missions.
# List all active missions
python telnyx_api.py list-state
# Get state for a specific mission
python telnyx_api.py get-state "find-window-washing-contractors"
# Remove a mission from state
python telnyx_api.py remove-state "find-window-washing-contractors"
python telnyx_api.py create-mission "Brief descriptive name" "Full description of the task"
Save the returned mission_id - you'll need it for all subsequent calls.
python telnyx_api.py create-run <mission_id> '{"original_request": "The exact user request", "context": "Any relevant context"}'
Save the returned run_id.
Before executing, outline your plan:
python telnyx_api.py create-plan <mission_id> <run_id> '[
{"step_id": "step_1", "description": "Research contractors online", "sequence": 1},
{"step_id": "step_2", "description": "Create voice agent for calls", "sequence": 2},
{"step_id": "step_3", "description": "Schedule calls to each contractor", "sequence": 3},
{"step_id": "step_4", "description": "Monitor call completions", "sequence": 4},
{"step_id": "step_5", "description": "Analyze results and select best options", "sequence": 5}
]'
python telnyx_api.py update-run <mission_id> <run_id> running
Use the init command to create mission, run, plan, and set status in one step:
python telnyx_api.py init "Find window washing contractors" "Find contractors in Chicago, call them, negotiate rates" "User wants window washing quotes" '[
{"step_id": "research", "description": "Find contractors online", "sequence": 1},
{"step_id": "setup", "description": "Create voice agent", "sequence": 2},
{"step_id": "calls", "description": "Schedule and make calls", "sequence": 3},
{"step_id": "analyze", "description": "Analyze results", "sequence": 4}
]'
This also automatically resumes if a mission with the same name already exists.
When your task requires making calls or sending SMS, create an AI assistant first.
For phone calls:
python telnyx_api.py create-assistant "Contractor Outreach Agent" "You are calling on behalf of [COMPANY]. Your goal is to [SPECIFIC GOAL]. Be professional and concise. Collect: [WHAT TO COLLECT]. If they cannot talk now, ask for a good callback time." "Hi, this is an AI assistant calling on behalf of [COMPANY]. Is this [BUSINESS NAME]? I am calling to inquire about your services. Do you have a moment?" '["telephony"]'
For SMS:
python telnyx_api.py create-assistant "SMS Outreach Agent" "You send SMS messages to collect information. Keep messages brief and professional." "Hi! I am reaching out on behalf of [COMPANY] regarding [PURPOSE]. Could you please reply with [REQUESTED INFO]?" '["messaging"]'
Save the returned assistant_id.
python telnyx_api.py list-phones --available
Or get the first available one directly:
python telnyx_api.py get-available-phone
If no phone numbers are available, STOP and inform the user:
"No available phone numbers found. You need to purchase phone numbers from Telnyx at https://portal.telnyx.com before I can make calls."
# For voice calls
python telnyx_api.py get-connection-id <assistant_id> telephony
# For SMS
python telnyx_api.py get-connection-id <assistant_id> messaging
# For voice calls
python telnyx_api.py assign-phone <phone_number_id> <connection_id> voice
# For SMS
python telnyx_api.py assign-phone <phone_number_id> <connection_id> sms
Use the setup-agent command to create assistant and assign phone number:
python telnyx_api.py setup-agent "find-window-washing-contractors" "Contractor Caller" "You are calling to get quotes for commercial window washing. Ask about: rates per floor, availability, insurance. Be professional." "Hi, I am calling to inquire about your commercial window washing services. Do you have a moment to discuss rates?"
This automatically:
IMPORTANT: After creating an assistant, you MUST link it to the mission run. This allows the system to track which agents are working on which missions.
If using setup-agent command: Linking is done automatically when mission_id and run_id are in the state.
If setting up manually:
python telnyx_api.py link-agent <mission_id> <run_id> <assistant_id>
You can also list and unlink agents:
# List all agents linked to a run
python telnyx_api.py list-linked-agents <mission_id> <run_id>
# Unlink an agent from a run
python telnyx_api.py unlink-agent <mission_id> <run_id> <assistant_id>
python telnyx_api.py log-event <mission_id> <run_id> custom "Created voice assistant and assigned phone number" "setup" '{"assistant_id": "<assistant_id>", "phone_number": "+15551234567", "type": "telephony"}'
Search for the information you need (contractors, leads, etc.):
python telnyx_api.py log-event <mission_id> <run_id> tool_call "Searching for window washing contractors in Chicago" "research" '{"tool": "WebSearch", "query": "commercial window washing contractors Chicago"}'
CRITICAL: Before scheduling calls, consider business hours:
scheduled_at_fixed_datetime must be in the future (at least 1 minute from now)python telnyx_api.py schedule-call <assistant_id> "+15551234567" "+15559876543" "2024-12-01T14:30:00Z" <mission_id> <run_id> <step_id>
Save the returned scheduled_event_id.
python telnyx_api.py schedule-sms <assistant_id> "+15551234567" "+15559876543" "2024-12-01T14:30:00Z" "Hi! I am reaching out on behalf of [COMPANY] to inquire about your window cleaning rates for commercial buildings. Could you share your pricing?" <mission_id> <run_id> <step_id>
python telnyx_api.py log-event <mission_id> <run_id> custom "Scheduled call to ABC Window Cleaning for 2:30 PM" "calls" '{"scheduled_event_id": "<event_id>", "contractor": "ABC Window Cleaning", "phone": "+15551234567", "scheduled_for": "2024-12-01T14:30:00Z"}'
After a call is scheduled, you need to poll for completion.
python telnyx_api.py get-event <assistant_id> <scheduled_event_id>
The event-level status tracks the overall lifecycle:
| Status | Meaning | Action |
|--------|---------|--------|
| pending | Waiting for scheduled time | Wait and check again later |
| in_progress | Call/SMS in progress | Check again in a few minutes |
| completed | Finished successfully | Get conversation_id, fetch insights |
| failed | Failed after retries | Consider rescheduling |
The call_status field provides the telephony-level outcome. This is the most important field for deciding what to do next.
| call_status | Meaning | Action |
|-------------|---------|--------|
| ringing | Phone is ringing, not yet answered | Still in progress — wait and poll again in 1-2 minutes |
| in-progress | Call is active, conversation ongoing | Still in progress — poll again in 2-3 minutes |
| completed | Call connected and finished normally | Success — get conversation_id, fetch insights |
| no-answer | Phone rang but nobody picked up | Retryable — reschedule for a different time |
| busy | Line is busy | Retryable — reschedule in 10-15 minutes, line may free up quickly |
| canceled | Call was canceled | Check if you canceled it; if not, may need to reschedule |
| failed | Call failed (network/system error) | Retryable — reschedule after a short backoff (5-10 minutes) |
When to start polling: A few minutes after scheduled_at_fixed_datetime
Polling intervals based on call_status:
ringing → poll again in 1-2 minutes (call may connect any moment)in-progress → poll again in 2-3 minutes (conversation is happening)pending (event status) → poll every 5 minutes until scheduled time passesno-answer / busy / failed → stop polling, handle retry immediatelycompleted → done, proceed to fetch insightsTimeout: If still ringing or in-progress after 30 minutes, treat as failed.
When call_status is no-answer, busy, or failed, the call can be retried:
busy → retry in 10-15 minutes (line may free up)no-answer → retry in 30 minutes to 2 hours (try a different time of day)failed → retry in 5-10 minutes (transient error)# Update tracker
python telnyx_api.py save-memory "<slug>" "call_tracker" '{"+15551234567": {"status": "no_answer", "attempts": 1, "call_status": "no-answer", "next_retry": "2024-12-02T10:00:00Z"}}'
# Log the failure
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" custom "Call not answered (busy), scheduling retry #2" "calls" '{"phone": "+15551234567", "call_status": "busy", "attempt": 1}'
# Schedule retry
python telnyx_api.py schedule-call <assistant_id> "+15551234567" "+15559876543" "2024-12-02T10:00:00Z" <mission_id> <run_id> <step_id>
Once a call completes and you have a conversation_id, retrieve the conversation insights.
IMPORTANT: Always use insights to get the call summary. Do NOT fetch raw conversation messages - insights provide a structured summary of the conversation outcome.
python telnyx_api.py get-insights <conversation_id>
The insight may not be immediately ready after the call ends. You must poll until the insight status is "completed".
Polling strategy:
conversation_idExample polling flow:
# First attempt
python telnyx_api.py get-insights "conv_xyz"
# Output: Insight status: in_progress
# Wait 10 seconds, try again
python telnyx_api.py get-insights "conv_xyz"
# Output: Insight status: in_progress
# Wait 10 seconds, try again
python telnyx_api.py get-insights "conv_xyz"
# Output: Insight: Customer quoted $350 for a 10-story building...
By default, all conversations are named "Voice Assistant Conversation" which is useless for reviewing in the portal. Rename each conversation with a meaningful label after it completes:
python telnyx_api.py rename-conversation "<conv_id>" "Call 1: ABC Window Cleaning - $350/visit"
Best practice: Include the call sequence number, target name, and key outcome in the name. This makes conversations instantly identifiable when browsing the mission in the Telnyx portal.
python telnyx_api.py log-event <mission_id> <run_id> custom "Call completed with ABC Window Cleaning - quoted $350" "calls" '{"conversation_id": "<conv_id>", "contractor": "ABC Window Cleaning", "outcome": "success", "quote": "$350", "availability": "next week", "notes": "Willing to negotiate for recurring contracts"}'
Every mission MUST complete ALL of these before it can be considered done. Skipping any of these leaves the mission in a broken state in the Telnyx portal.
| # | Action | Command | Why |
|---|--------|---------|-----|
| 1 | Update ALL plan step statuses | update-step <mid> <rid> <step_id> completed | Steps show in portal — "pending" means it looks unfinished |
| 2 | Log events for every completed call | log-event ... custom "Call completed" <step_id> '{"conversation_id": "..."}' | Creates audit trail linking conversations to the mission |
| 3 | Set run result_summary | Included in complete or update-run | Human-readable summary visible in portal |
| 4 | Set run result_payload | Included in complete or update-run | Structured data for programmatic consumption |
| 5 | Mark run as succeeded/failed | complete or update-run <mid> <rid> succeeded | Closes the run |
The complete command handles #3, #4, and #5 in one call — but you still need to do #1 and #2 yourself.
# 1. Update every step status (do this THROUGHOUT the mission, not just at the end)
python telnyx_api.py update-step <mission_id> <run_id> "setup" "completed"
python telnyx_api.py update-step <mission_id> <run_id> "calls" "completed"
python telnyx_api.py update-step <mission_id> <run_id> "analyze" "completed"
# 2. Complete the run with summary and payload
python telnyx_api.py complete "<slug>" <mission_id> <run_id> \
"Contacted 5 contractors, received 4 quotes. Best: ABC Cleaning ($350)" \
'{"contractors_contacted": 5, "quotes_received": 4, "recommended": [{"name": "ABC Cleaning", "quote": 350}]}'
Common mistake: Logging step_completed events but NOT calling update-step. These are separate — events are the audit log, step status is the progress tracker. You need BOTH.
After all calls complete:
Use the complete command to set result_summary, result_payload, mark the run as succeeded, and clean up state in one step:
python telnyx_api.py complete "find-window-washing-contractors" <mission_id> <run_id> "Contacted 5 contractors, received 4 quotes. Best options: ABC Cleaning ($350) and XYZ Windows ($380)." '{"contractors_contacted": 5, "quotes_received": 4, "recommended": [{"name": "ABC Cleaning", "quote": 350}, {"name": "XYZ Windows", "quote": 380}]}'
Or use update-run directly with all fields:
python telnyx_api.py update-run <mission_id> <run_id> succeeded "Human-readable summary here" '{"structured": "payload"}'
The complete command also removes the mission from the state file.
Log EVERY action as an event for complete audit trail. Events are stored in the cloud and provide permanent backup even if local files are lost.
You MUST update the plan step status via update-step when starting or completing each step. Logging events alone does NOT change the step status — the client tracks progress by looking at step statuses, not events.
# When STARTING a step:
python telnyx_api.py update-step "$MISSION_ID" "$RUN_ID" "research" "in_progress"
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" step_started "Starting: Research contractors" "research"
# When COMPLETING a step:
python telnyx_api.py update-step "$MISSION_ID" "$RUN_ID" "research" "completed"
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" step_completed "Completed: Research contractors" "research"
# When a step FAILS:
python telnyx_api.py update-step "$MISSION_ID" "$RUN_ID" "calls" "failed"
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" error "Failed: Could not reach any contractors" "calls"
# To SKIP a step:
python telnyx_api.py update-step "$MISSION_ID" "$RUN_ID" "setup" "skipped"
Always call update-step BEFORE log-event — this ensures the step status is correct even if the event logging fails.
step_id is a required parameter - it links events to your plan steps, enabling tracking of which activities belong to which phase.
# With step_id (links to plan step)
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" custom "Found contractor" "research" '{"name": "ABC"}'
# Use "-" if event doesn't belong to a specific step
python telnyx_api.py log-event "$MISSION_ID" "$RUN_ID" custom "General note" "-" '{"note": "value"}'
The step_id should match one of the step_id values from your plan (e.g., "research", "setup", "calls", "analyze").
| Action | Step Status Update | Event Type | step_id | Example Summary |
|--------|-------------------|------------|---------|-----------------|
| Starting a plan step | update-step ... in_progress | step_started | step_id | "Starting: Research contractors" |
| Completing a step | update-step ... completed | step_completed | step_id | "Completed: Research contractors" |
| Step failed | update-step ... failed | error | step_id | "Failed: Could not reach contractors" |
| Web search | — | tool_call | "research" | "Searching for window cleaning contractors" |
| Creating assistant | — | custom | "setup" | "Created voice assistant: ast_123" |
| Scheduling call | — | custom | "calls" | "Scheduled call to ABC for 2:30 PM" |
| Call completed | — | custom | "calls" | "Call completed with ABC - got quote $350" |
| Call failed | — | error | "calls" | "Call to XYZ not answered after 3 attempts" |
| Decision made | — | message | "analyze" | "Selected ABC and XYZ as top choices" |
# Check setup
python telnyx_api.py check-key
# Missions
python telnyx_api.py create-mission <name> <instructions>
python telnyx_api.py get-mission <mission_id>
python telnyx_api.py list-missions
# Runs
python telnyx_api.py create-run <mission_id> <input_json>
python telnyx_api.py get-run <mission_id> <run_id>
python telnyx_api.py update-run <mission_id> <run_id> <status>
python telnyx_api.py list-runs <mission_id>
# Plan
python telnyx_api.py create-plan <mission_id> <run_id> <steps_json>
python telnyx_api.py get-plan <mission_id> <run_id>
python telnyx_api.py update-step <mission_id> <run_id> <step_id> <status>
# status: pending, in_progress, completed, skipped, failed
# Events (step_id is REQUIRED - use "-" if no specific step)
python telnyx_api.py log-event <mission_id> <run_id> <type> <summary> <step_id> [payload_json]
python telnyx_api.py list-events <mission_id> <run_id>
# Assistants
python telnyx_api.py list-assistants [--name=<filter>] [--page=<n>] [--size=<n>]
python telnyx_api.py create-assistant <name> <instructions> <greeting> [options_json]
python telnyx_api.py get-assistant <assistant_id>
python telnyx_api.py update-assistant <assistant_id> <updates_json>
python telnyx_api.py get-connection-id <assistant_id> [telephony|messaging]
# Phone Numbers
python telnyx_api.py list-phones [--available]
python telnyx_api.py get-available-phone
python telnyx_api.py assign-phone <phone_id> <connection_id> [voice|sms]
# Scheduled Events
python telnyx_api.py schedule-call <assistant_id> <to_phone> <from_phone> <datetime> <mission_id> <run_id> [step_id] [dynamic_variables_json]
python telnyx_api.py schedule-sms <assistant_id> <to_phone> <from_phone> <datetime> <text> [mission_id] [mission_run_id] [step_id] [dynamic_variables_json]
python telnyx_api.py get-event <assistant_id> <event_id>
python telnyx_api.py cancel-scheduled-event <assistant_id> <event_id>
python telnyx_api.py list-events-assistant <assistant_id>
# Insights (conversation results - POLL until status is "completed"!)
python telnyx_api.py get-insights <conversation_id>
python telnyx_api.py rename-conversation <conversation_id> <name>
# Insight Templates (CRUD)
python telnyx_api.py create-insight <name> <instructions> [options_json] # options: json_schema, webhook
python telnyx_api.py get-insight <insight_id>
python telnyx_api.py list-insights
python telnyx_api.py update-insight <insight_id> <updates_json>
# Insight Groups
python telnyx_api.py create-insight-group <name> [options_json] # options: description, webhook
python telnyx_api.py get-insight-group <group_id>
python telnyx_api.py list-insight-groups
python telnyx_api.py update-insight-group <group_id> <updates_json>
python telnyx_api.py assign-insight <group_id> <insight_id>
python telnyx_api.py unassign-insight <group_id> <insight_id>
# Mission Run Agents (linking agents to runs)
python telnyx_api.py link-agent <mission_id> <run_id> <telnyx_agent_id>
python telnyx_api.py list-linked-agents <mission_id> <run_id>
python telnyx_api.py unlink-agent <mission_id> <run_id> <telnyx_agent_id>
# State Management
python telnyx_api.py list-state
python telnyx_api.py get-state <slug>
python telnyx_api.py remove-state <slug>
# Memory (SAVE OFTEN!)
python telnyx_api.py save-memory <slug> <key> <value_json>
python telnyx_api.py get-memory <slug> [key]
python telnyx_api.py append-memory <slug> <key> <item_json>
# High-Level Workflows
python telnyx_api.py init <name> <instructions> <request> [steps_json]
python telnyx_api.py setup-agent <slug> <name> <instructions> <greeting>
python telnyx_api.py complete <slug> <mission_id> <run_id> <summary> [payload_json]
# 1. Initialize the mission (creates mission, run, plan, sets to running)
python telnyx_api.py init "Find window washing contractors" \
"Find contractors in Chicago, call them, negotiate rates, select best two" \
"Find me window washing contractors in Chicago" \
'[{"step_id": "research", "description": "Find contractors online", "sequence": 1}, {"step_id": "setup", "description": "Create voice agent", "sequence": 2}, {"step_id": "calls", "description": "Schedule and make calls", "sequence": 3}, {"step_id": "analyze", "description": "Analyze results", "sequence": 4}]'
# Output: Created mission: mis_abc123
# Created run: run_def456
# 2. Get the mission slug and IDs from state
python telnyx_api.py get-state "find-window-washing-contractors"
# 3. Mark research step as in_progress and start working
python telnyx_api.py update-step "mis_abc123" "run_def456" "research" "in_progress"
python telnyx_api.py log-event "mis_abc123" "run_def456" step_started "Starting: Find contractors online" "research"
# 4. Setup voice agent (creates assistant, links to run, assigns phone number)
python telnyx_api.py update-step "mis_abc123" "run_def456" "setup" "in_progress"
python telnyx_api.py setup-agent "find-window-washing-contractors" \
"Contractor Caller" \
"You are calling to get quotes for commercial window washing. Ask about: rates per floor, availability, insurance. Be professional." \
"Hi, I am calling to inquire about your commercial window washing services. Do you have a moment to discuss rates?"
# Output: Created assistant: ast_xyz789
# Linked agent ast_xyz789 to run run_def456
# Found available: +15559876543
# Assigned phone number 123456
python telnyx_api.py update-step "mis_abc123" "run_def456" "setup" "completed"
python telnyx_api.py log-event "mis_abc123" "run_def456" step_completed "Completed: Voice agent setup" "setup"
# 5. Get agent phone from state
AGENT_PHONE=$(python telnyx_api.py get-state "find-window-washing-contractors" | python -c "import sys,json; print(json.load(sys.stdin).get('agent_phone',''))")
ASSISTANT_ID=$(python telnyx_api.py get-state "find-window-washing-contractors" | python -c "import sys,json; print(json.load(sys.stdin).get('assistant_id',''))")
# 6. After research, SAVE to memory AND log events with step_id (CRITICAL!)
python telnyx_api.py append-memory "find-window-washing-contractors" "contractors_found" '{"name": "ABC Cleaning", "phone": "+13125551234", "source": "web search"}'
python telnyx_api.py log-event "mis_abc123" "run_def456" custom "Found contractor: ABC Cleaning" "research" '{"name": "ABC Cleaning", "phone": "+13125551234"}'
python telnyx_api.py append-memory "find-window-washing-contractors" "contractors_found" '{"name": "XYZ Windows", "phone": "+13125555678", "source": "web search"}'
python telnyx_api.py log-event "mis_abc123" "run_def456" custom "Found contractor: XYZ Windows" "research" '{"name": "XYZ Windows", "phone": "+13125555678"}'
# 7. Complete research step, start calls step
python telnyx_api.py update-step "mis_abc123" "run_def456" "research" "completed"
python telnyx_api.py log-event "mis_abc123" "run_def456" step_completed "Completed: Found 2 contractors" "research"
python telnyx_api.py update-step "mis_abc123" "run_def456" "calls" "in_progress"
python telnyx_api.py log-event "mis_abc123" "run_def456" step_started "Starting: Schedule calls" "calls"
# 8. Schedule calls
python telnyx_api.py schedule-call "$ASSISTANT_ID" "+13125551234" "$AGENT_PHONE" "2024-12-01T15:00:00Z" "$MISSION_ID" "$RUN_ID" "$STEP_ID"
# Output: Scheduled call: evt_abc123
# 9. SAVE scheduled event to memory AND log event with step_id (CRITICAL!)
python telnyx_api.py append-memory "find-window-washing-contractors" "calls_scheduled" '{"event_id": "evt_abc123", "contractor": "ABC Cleaning", "scheduled_for": "2024-12-01T15:00:00Z"}'
python telnyx_api.py log-event "mis_abc123" "run_def456" custom "Scheduled call to ABC Cleaning for 3:00 PM" "calls" '{"scheduled_event_id": "evt_abc123", "contractor": "ABC Cleaning"}'
# 10. Poll for completion (after scheduled time)
python telnyx_api.py get-event "$ASSISTANT_ID" "evt_abc123"
# Output: Status: completed, conversation_id: conv_xyz
# 11. Get insights - POLL UNTIL STATUS IS "completed"
python telnyx_api.py get-insights "conv_xyz"
# Output: Insight status: in_progress
# (wait 10 seconds and retry)
python telnyx_api.py get-insights "conv_xyz"
# Output: Insight status: in_progress
# (wait 10 seconds and retry)
python telnyx_api.py get-insights "conv_xyz"
# Output: Insight: Customer quoted $350 for a 10-story building. Available next week.
# (status is now "completed" - proceed with the insight data)
# 12. SAVE call results to memory AND log event with step_id (CRITICAL!)
python telnyx_api.py save-memory "find-window-washing-contractors" "call_results" '{"ABC Cleaning": {"status": "completed", "conversation_id": "conv_xyz", "quote": 350, "availability": "next week"}}'
python telnyx_api.py log-event "mis_abc123" "run_def456" custom "Call completed: ABC Cleaning quoted $350, available next week" "calls" '{"contractor": "ABC Cleaning", "quote": 350, "conversation_id": "conv_xyz"}'
# 13. Complete calls step, start analyze step
python telnyx_api.py update-step "mis_abc123" "run_def456" "calls" "completed"
python telnyx_api.py log-event "mis_abc123" "run_def456" step_completed "Completed: All calls done" "calls"
python telnyx_api.py update-step "mis_abc123" "run_def456" "analyze" "in_progress"
python telnyx_api.py log-event "mis_abc123" "run_def456" step_started "Starting: Analyze results" "analyze"
# 14. Complete the mission (mark analyze step done first)
python telnyx_api.py update-step "mis_abc123" "run_def456" "analyze" "completed"
python telnyx_api.py log-event "mis_abc123" "run_def456" step_completed "Completed: Analysis done" "analyze"
python telnyx_api.py complete "find-window-washing-contractors" "mis_abc123" "run_def456" \
"Found 2 best contractors: ABC ($350) and XYZ ($380)" \
'{"recommended": ["ABC Cleaning", "XYZ Windows"]}'
# Output: Updated run run_def456: succeeded
# Mission 'find-window-washing-contractors' completed successfully
Always check what already exists before creating new assistants, insights, or insight groups. Reuse is better than duplication.
Run these commands at the start of every mission to inventory what's available:
# 1. Search for existing assistants by name — maybe one already fits your use case
python telnyx_api.py list-assistants --name=Weather
python telnyx_api.py list-assistants # or list all (paginated)
python telnyx_api.py list-assistants --page=2 # next page
# 2. List existing insight templates — reuse structured insights across missions
python telnyx_api.py list-insights
# 3. List existing insight groups — you may only need to add an insight to an existing group
python telnyx_api.py list-insight-groups
# 4. List available phone numbers — check what's already assigned vs free
python telnyx_api.py list-phones --available
All list commands are paginated. If you have many resources, page through with --page=N. The assistant name filter does substring matching — use it to quickly find relevant assistants instead of scrolling through pages.
The rule is: reuse existing resources IF you can use them as-is. Do NOT modify existing assistants, insights, or insight groups that may be in use by other missions or users. Editing a shared resource (e.g., changing an assistant's instructions or an insight's schema) can silently break unrelated workflows that depend on the current configuration.
Safe to reuse without modification:
When to create new instead of reusing:
For dynamic context between calls (e.g., Class 3 Sequential Negotiation, where you inject "best quote so far" into each call), use dynamic variables passed via the scheduled events API rather than modifying the assistant. Define variable placeholders in the assistant's instructions (e.g., {{best_quote}}) and pass the values at schedule time. This keeps the assistant immutable while varying context per call.
Assistants: Search for existing assistants. If one matches your needs exactly (instructions, tools, voice, model), reuse it. If it's close but not quite right, create a new one — don't modify the existing one:
python telnyx_api.py list-assistants --name=Weather # search by name
python telnyx_api.py get-assistant <id> # inspect full config before deciding
# If it fits → reuse as-is
# If it doesn't fit → create a new assistant instead
Insights: A structured insight like "extract high temperature and snow chance" is reusable across many missions. Check list-insights before creating a new one. If a good insight exists with the right schema, assign it to your group. If you need a different schema, create a new insight template — don't modify the existing one.
Insight Groups: Create a new group per mission (they're cheap), but populate it with existing insight templates when they match. Only create new insight templates when your data extraction needs are genuinely new.
Phone Numbers: Numbers already assigned to a connection can't be reused for a different assistant. Only grab unassigned numbers.
Not all missions are the same. Before planning, identify which class your mission falls into — it determines whether calls run in parallel or serial, how state flows between calls, and whether human gates are needed.
Does call N depend on results of call N-1?
YES → Is it negotiation (leveraging previous results)?
YES → Class 3: Sequential Negotiation
NO → Does it have distinct rounds with human approval?
YES → Class 4: Multi-Round / Follow-up
NO → Class 5: Information Gathering → Action
NO → Do you need structured scoring/ranking?
YES → Class 2: Parallel Screening with Rubric
NO → Class 1: Parallel Sweep
Fan out calls in parallel batches. Every call asks the same question. No call depends on another's results. Collect all answers, then compare.
Goal: Call 10 weather stations, extract today's high temperature, compare.
# Plan steps
[
{"step_id": "setup", "description": "Create IVR assistant with DTMF + structured insight", "sequence": 1},
{"step_id": "calls", "description": "Schedule calls to all 10 stations", "sequence": 2},
{"step_id": "poll", "description": "Poll for completion and collect insights", "sequence": 3},
{"step_id": "analyze", "description": "Compare temperatures, find hottest/coldest", "sequence": 4}
]
# Insight schema
{"type": "object", "properties": {
"location": {"type": "string"},
"high_temp_f": {"type": "number"},
"snow_mentioned": {"type": "boolean"},
"forecast_summary": {"type": "string"}
}, "required": ["location", "high_temp_f"]}
# Flow:
# 1. Create assistant with send_dtmf tool + insight group
# 2. Schedule all 10 calls in one batch (staggered 1 min apart)
# 3. Cron job polls every 2 min, collects structured insights
# 4. When all done: compare high_temp_f across all results, report hottest city
Goal: Call 10 retail stores to confirm holiday hours.
# Same pattern: single assistant, all calls in parallel
# Assistant instructions: "You are calling to confirm store hours for [HOLIDAY].
# Ask: What time do you open and close on [DATE]? Are you open at all?"
# Insight schema:
{"type": "object", "properties": {
"store_name": {"type": "string"},
"is_open": {"type": "boolean"},
"open_time": {"type": "string"},
"close_time": {"type": "string"},
"notes": {"type": "string"}
}, "required": ["store_name", "is_open"]}
# Schedule all 10 calls → poll → compare → report table of hours
Fan out calls in parallel, but each call follows a structured rubric. Results are scored automatically via structured insights, then ranked post-hoc.
Goal: Call 10 restaurants, score on availability/price/ambiance, rank top 3.
# Insight schema (the rubric)
{"type": "object", "properties": {
"restaurant_name": {"type": "string"},
"has_availability": {"type": "boolean", "description": "Table available for requested date/time/party size"},
"availability_score": {"type": "integer", "description": "1-5, 5 = exact time available, 1 = nothing close"},
"price_range": {"type": "string", "enum": ["$", "$$", "$$$", "$$$$"]},
"estimated_per_person": {"type": "number"},
"ambiance_notes": {"type": "string"},
"ambiance_score": {"type": "integer", "description": "1-5 based on description of atmosphere"},
"wait_time_minutes": {"type": "number"},
"overall_score": {"type": "integer", "description": "1-10 overall recommendation"}
}, "required": ["restaurant_name", "has_availability", "availability_score", "price_range", "overall_score"]}
# Assistant instructions:
# "You are calling to check availability for dinner Friday at 7pm, party of 4.
# Ask about: availability, approximate price per person, dress code/atmosphere,
# expected wait time. Be conversational and polite."
# Flow:
# 1. Create insight template with rubric schema
# 2. Create assistant with insight group wired up
# 3. Schedule all 10 calls in parallel
# 4. Collect structured insights → sort by overall_score desc → report top 3
Goal: Phone-screen 10 candidates, score on communication/experience/culture-fit, shortlist top 3.
# Insight schema (the rubric)
{"type": "object", "properties": {
"candidate_name": {"type": "string"},
"communication_score": {"type": "integer", "description": "1-5, clarity and professionalism"},
"experience_years": {"type": "number"},
"relevant_experience_score": {"type": "integer", "description": "1-5, relevance to the role"},
"culture_fit_score": {"type": "integer", "description": "1-5, enthusiasm and alignment"},
"salary_expectation": {"type": "number"},
"available_start_date": {"type": "string"},
"red_flags": {"type": "string", "description": "Any concerns noted"},
"overall_score": {"type": "integer", "description": "1-10 overall recommendation"}
}, "required": ["candidate_name", "communication_score", "relevant_experience_score", "culture_fit_score", "overall_score"]}
# Assistant instructions cover specific screening questions for the role
# All 10 calls run in parallel → rank by overall_score → shortlist top 3
Calls MUST run serially. Each call's strategy depends on previous results. You're leveraging information from earlier calls to get better outcomes in later ones.
⚠️ NEVER parallelize these. The entire value comes from sequential information advantage.
dynamic_variables in the scheduled event to inject context per call — no need to modify the assistant between calls{{best_quote}} and {{best_company}} placeholders in instructions once, then pass different values per call via the scheduled events APIGoal: Call 5 roofers sequentially, negotiate each against the best previous quote.
# Plan steps
[
{"step_id": "setup", "description": "Create assistant + find roofers", "sequence": 1},
{"step_id": "call-1", "description": "Call roofer 1 (baseline)", "sequence": 2},
{"step_id": "call-2", "description": "Call roofer 2 with context", "sequence": 3},
{"step_id": "call-3", "description": "Call roofer 3 with context", "sequence": 4},
{"step_id": "call-4", "description": "Call roofer 4 with context", "sequence": 5},
{"step_id": "call-5", "description": "Call roofer 5 with context", "sequence": 6},
{"step_id": "analyze", "description": "Select best deal", "sequence": 7}
]
# Flow:
# 1. Create assistant with dynamic variable placeholders in instructions:
# "Ask for a quote for roof repair on a 2000 sq ft home. Get price, timeline, warranty.
# {{#best_quote}}
# CONTEXT: You have received a quote of {{best_quote}} from {{best_company}}.
# Mention this if the price seems high. Ask if they can match or beat it.
# {{/best_quote}}"
# Set dynamic_variables: {"best_quote": null, "best_company": null}
# 2. Call roofer 1 (no leverage yet — best_quote is null, so that section is skipped)
# python telnyx_api.py schedule-call <id> "+1555..." "+1555..." "<time>" $MISSION_ID $RUN_ID $STEP_ID
# → get insight → save quote ($500)
# python telnyx_api.py save-memory "<slug>" "best_quote" '{"amount": 500, "company": "Roofer 1"}'
# 3. Call roofer 2 — pass dynamic variables via scheduled event:
# python telnyx_api.py schedule-call <id> "+1555..." "+1555..." "<time>" $MISSION_ID $RUN_ID $STEP_ID \
# '{"best_quote": "$500", "best_company": "Roofer 1"}'
# → get insight → if better ($420), update best_quote
# 4. Call roofer 3 — pass updated context:
# python telnyx_api.py schedule-call <id> "+1555..." "+1555..." "<time>" $MISSION_ID $RUN_ID $STEP_ID \
# '{"best_quote": "$420", "best_company": "Roofer 2"}'
# 5. Repeat: schedule with new dynamic vars → poll → insight → update state → next
# 6. After all 5: report best deal with full comparison
Goal: Call 4 insurance providers, use each quote to leverage the next.
# Ordering strategy: Start with the provider you care least about (get a baseline),
# end with your preferred provider (maximum leverage).
# Assistant instructions use dynamic variable placeholders:
# "You are calling about auto insurance for a 2022 Toyota Camry.
# {{#best_quote}}
# LEVERAGE: The best quote so far is {{best_quote}}/month
# from {{best_company}}. Mention this and ask them to beat it.
# {{/best_quote}}"
# Sequential pattern — pass dynamic variables per call:
# Call 1 (baseline): no leverage vars → "What's your rate for [coverage details]?"
# Call 2: {"best_quote": "$180", "best_company": "Geico"}
# Call 3: {"best_quote": "$155", "best_company": "Progressive"}
# Call 4 (preferred): {"best_quote": "$145", "best_company": "StateFarm"}
The mission has distinct phases. Round 1 is broad outreach. Results are analyzed, a human approves the shortlist, then Round 2 does deep-dive calls with a different focus.
round-1-calls, round-1-analysis, human-approval, round-2-callsGoal: Screen 10 vendors on basic criteria, shortlist top 3 with human approval, deep-dive on technical specs.
# Plan steps
[
{"step_id": "setup", "description": "Create screening assistant + insight rubric", "sequence": 1},
{"step_id": "round-1-calls", "description": "Screen all 10 vendors", "sequence": 2},
{"step_id": "round-1-analysis", "description": "Rank and shortlist top 3", "sequence": 3},
{"step_id": "human-approval", "description": "DM human with top 3, wait for approval", "sequence": 4},
{"step_id": "round-2-setup", "description": "Create deep-dive assistant with technical questions", "sequence": 5},
{"step_id": "round-2-calls", "description": "Deep-dive calls with approved vendors", "sequence": 6},
{"step_id": "final-analysis", "description": "Final recommendation", "sequence": 7}
]
# Round 1 assistant: "Ask about pricing, lead time, minimum order, and general capabilities."
# Round 1 insight rubric: price_score, lead_time_days, meets_minimum, capability_match
# After Round 1 completes:
# → Rank by scores → DM human:
# "Round 1 complete. Top 3 vendors:
# 1. VendorA — score 8.5, $12/unit, 2-week lead
# 2. VendorB — score 7.8, $14/unit, 1-week lead
# 3. VendorC — score 7.2, $11/unit, 3-week lead
# Approve these for Round 2? Reply YES or adjust."
# Wait for human response (pause cron or check for reply)
# Round 2 assistant (different!): "Ask detailed technical questions:
# API integration support? SLA guarantees? Disaster recovery?
# Reference customers we can contact?"
# Round 2 insight rubric: api_support, sla_score, dr_plan, references_provided
Goal: Phone-screen 15 candidates (Round 1), shortlist 5 (human approval), detailed interviews (Round 2).
# Round 1: Quick 5-minute screen — "Tell me about your background, why this role,
# salary expectations, availability."
# Insight rubric: communication_score, experience_match, salary_in_range, enthusiasm
# → Rank → DM human with top 5 + scores
# → Human approves (or swaps in someone from position 6-7)
# Round 2: 15-minute deep dive — completely different assistant:
# "Ask about: specific project experience with [TECH], how they handle conflict,
# a time they failed and what they learned, questions they have for us."
# Different insight rubric: technical_depth, problem_solving, culture_fit, curiosity
# Track in memory:
python telnyx_api.py save-memory "<slug>" "rounds" '{
"round_1": {"candidates": [...], "advanced": ["+1555...", "+1555..."]},
"round_2": {"candidates": [...], "results": [...]}
}'
Call to collect information, but the mission doesn't just report — it takes action based on results. Once you find what you need, stop searching and act.
list-events-assistant to find pending events, then cancel-scheduled-event to cancel each one.update-assistant to switch modes.Goal: Call restaurants until one has availability Friday 7pm for 4, then book it.
# Plan steps
[
{"step_id": "setup", "description": "Create screening assistant", "sequence": 1},
{"step_id": "screen", "description": "Call restaurants to check availability", "sequence": 2},
{"step_id": "book", "description": "Book at first available restaurant", "sequence": 3}
]
# Flow:
# 1. Create assistant: "Call and ask if they have a table for 4 this Friday at 7pm.
# If yes, say you'd like to book it under the name [NAME]. Confirm the reservation.
# If no, ask about Saturday instead, then politely end the call."
# 2. Schedule calls to 5 restaurants (parallel — screening doesn't depend on each other)
# 3. Poll results as they come in:
# - Restaurant A: no availability → continue
# - Restaurant B: has availability, BOOKED! → SUCCESS
# - Cancel/ignore remaining calls (Restaurants C, D, E)
# - If none of the 5 work: expand to 5 more restaurants (fallback)
# Insight schema:
{"type": "object", "properties": {
"restaurant_name": {"type": "string"},
"has_availability": {"type": "boolean"},
"reservation_confirmed": {"type": "boolean"},
"reservation_time": {"type": "string"},
"reservation_name": {"type": "string"},
"confirmation_number": {"type": "string"},
"alternative_offered": {"type": "string"}
}, "required": ["restaurant_name", "has_availability", "reservation_confirmed"]}
# Early termination: when reservation_confirmed = true, mission succeeds
Goal: Call dentist offices until one has a slot this week, confirm the appointment.
# Same pattern as restaurant booking:
# 1. Screen in parallel: "Do you have any openings this week for a cleaning?"
# 2. First office with availability: "I'd like to book that slot for [NAME], DOB [DOB]."
# 3. Stop remaining calls once booked.
# Key difference: may need to provide insurance info, patient details.
# Assistant instructions include all necessary details upfront.
# Fallback: if no office has availability this week, expand search to next week
# or expand to more offices.
These patterns apply across multiple mission classes.
Use dynamic variables passed via the scheduled events API to inject context from previous call results into each call. This keeps the assistant immutable — define {{variable}} placeholders in instructions once, then pass different values per scheduled event. Essential for Class 3 (Sequential Negotiation) and Class 5 (action phase).
# Assistant instructions use placeholders:
# "You are calling to get a quote for roof repair on a 2000 sq ft home.
# {{#best_quote}}
# IMPORTANT CONTEXT: Another contractor has quoted {{best_quote}}.
# If this contractor quotes higher, mention you have a better offer and ask if
# they can match or beat it. Be professional but firm.
# {{/best_quote}}"
# After getting a quote of $350 from call 1, pass it as a dynamic variable on call 2:
python telnyx_api.py schedule-call <assistant_id> "+1555..." "+1555..." "<time>" <mission_id> <run_id> <step_id> '{"best_quote": "$350", "best_company": "ABC Roofing"}'
Between rounds or before taking action, DM the human and pause.
# Pattern:
# 1. Cron job detects Round 1 complete
# 2. Formats results summary
# 3. Sends message to human via Telegram/Slack
# 4. Saves state: {"awaiting_approval": true, "approval_summary": "..."}
# 5. Cron continues to poll but takes no action until human responds
# 6. Human replies "approved" or "change X" → cron detects reply → proceeds
When the goal is met, stop unnecessary remaining calls.
# Check for pending events:
python telnyx_api.py list-events-assistant <assistant_id>
# Cancel each pending event:
python telnyx_api.py cancel-scheduled-event <assistant_id> <event_id>
For Sequential Negotiation (Class 3), order matters:
This section covers real-world considerations for running voice missions at scale. Read this before planning any mission involving outbound calls.
The send_dtmf tool is included by default on all assistants created by this skill. Most outbound calls — even to businesses with live staff — will hit an IVR or phone tree first before reaching a person. Without send_dtmf, the assistant can't navigate these menus and the call will fail or time out.
If you explicitly pass a custom tools list, make sure to include send_dtmf unless you have a specific reason not to.
Noise suppression is disabled by default for mission assistants. Aggressive noise suppression (krisp, deepfilternet) can interfere with IVR tones, hold music, and low-quality phone audio, causing the assistant to miss information. Only enable it if you're in a known-noisy environment and calling humans directly.
Tip: Some numbers include required button presses in their listing (e.g., 602-275-0073(#4)). Include these in the assistant's instructions: "After connecting, press # then 4 to reach the local forecast."
Expect IVRs even when calling businesses. Most restaurants, hotels, and offices have a phone tree. Instruct the assistant: "If you reach an automated menu, press 0 or say 'representative' to reach a person. If you hear options, press the one for reservations/front desk."
Always prefer structured insights over raw transcripts for analyzing call results. Insights give you machine-readable, consistent data. Transcripts are noisy and require parsing.
Step 1: Create a structured insight template with a JSON schema defining exactly what to extract:
python telnyx_api.py create-insight "Weather Forecast Data" \
"Extract weather forecast information from the recorded forecast. Listen for today's high temperature, any mention of snow or snow probability, and a brief summary of conditions." \
'{"json_schema": {"type": "object", "properties": {"location": {"type": "string", "description": "City or region name"}, "high_temp_f": {"type": "number", "description": "Todays high temperature in Fahrenheit"}, "low_temp_f": {"type": "number", "description": "Todays low temperature in Fahrenheit"}, "snow_mentioned": {"type": "boolean", "description": "Whether snow was mentioned in the forecast"}, "snow_chance_pct": {"type": "number", "description": "Snow probability percentage if mentioned, null otherwise"}, "forecast_summary": {"type": "string", "description": "Brief summary of forecast conditions"}}, "required": ["location", "high_temp_f", "snow_mentioned"]}}'
Step 2: Create an insight group and assign the insight:
python telnyx_api.py create-insight-group "Weather Mission Insights"
python telnyx_api.py assign-insight <group_id> <insight_id>
Step 3: Wire the group to the assistant via insight_settings:
python telnyx_api.py create-assistant "Weather Listener" \
"..." "..." \
'{"features": ["telephony"], "insight_settings": {"insight_group_id": "<group_id>"}}'
Now every completed call automatically produces structured JSON you can compare across calls — no transcript parsing needed.
When creating structured insights, always also add an unstructured "Conversation Summary" insight to the group. Structured schemas only capture what you explicitly define — if the conversation reveals something unexpected (an alternative time offered, a special event mentioned, a caveat), the structured insight will miss it.
Every account has a default "Summary" insight — search for it and reuse it instead of creating a new one:
# 1. Find the default Summary insight (may be on later pages — paginate!)
python telnyx_api.py list-insights
python telnyx_api.py list-insights --page=2
# Look for one named "Summary" (unstructured) — it's typically one of the earliest created
# 2. Only if you can't find it, create one:
python telnyx_api.py create-insight "Conversation Summary" \
"Provide a detailed summary of the entire conversation. Include everything discussed: what was asked, what was answered, any alternatives offered, tone of the interaction, and any information that might be useful but wasn't explicitly asked about. Be thorough — this is the safety net for anything the structured extraction might miss."
# 3. Add BOTH structured + summary to the insight group
python telnyx_api.py assign-insight <group_id> <structured_insight_id>
python telnyx_api.py assign-insight <group_id> <summary_insight_id>
This way you get clean structured data for ranking/comparison AND a full narrative you can fall back on.
Use unstructured (free-text) insights when:
Use structured insights when:
Be aware of your Telnyx account's concurrent call limits. Firing 100 outbound calls simultaneously will hit rate limits and fail.
Batch 1: Numbers 1-10 → schedule at T+0, T+1m, T+2m, ... T+9m
Wait for Batch 1 completion (poll all events)
Batch 2: Numbers 11-20 → schedule at T+0, T+1m, ...
...
The assistant supports voicemail detection via telephony_settings.voicemail_detection. Use it wisely based on your target:
on_voicemail_detected action:
stop_assistant — just hang up, don't leave a messageleave_message_and_stop_assistant — leave a voicemail, then hang up# Just hang up on voicemail (no message)
python telnyx_api.py create-assistant "Human Caller" "..." "..." \
'{"features": ["telephony"], "telephony_settings": {"voicemail_detection": {"on_voicemail_detected": {"action": "stop_assistant"}}}}'
# Leave a voicemail message, then hang up
python telnyx_api.py create-assistant "Human Caller" "..." "..." \
'{"features": ["telephony"], "telephony_settings": {"voicemail_detection": {"on_voicemail_detected": {"action": "leave_message_and_stop_assistant", "voicemail_message": {"type": "prompt", "prompt": "Leave a brief message explaining why you called and ask them to call back."}}}}}'
How to disable AMD: Set the action to continue_assistant — this tells the assistant to keep going even if voicemail/machine is detected. Do NOT set voicemail_detection to null — that may not stick or may revert to defaults on some API operations.
python telnyx_api.py update-assistant <assistant_id> '{"telephony_settings": {"voicemail_detection": {"on_voicemail_detected": {"action": "continue_assistant"}}}}'
Rule of thumb: If there's any chance the call will be answered by an automated greeting, phone tree, or IVR — even a simple "press 1 for reservations" — disable AMD. It's better to handle a voicemail manually than to have AMD block your assistant from navigating a phone menu.
After scheduling calls, do not block the main session with polling loops. Instead, set up a cron job to poll for results periodically.
CRITICAL: The polling cron job must ONLY:
Never schedule new calls without checking the tracker first. The cron should be idempotent — running it twice with the same state must not produce duplicate calls. Always check status and attempts before scheduling anything.
Always clean up: The cron MUST remove itself when the mission is complete. Include the cron job ID in the task prompt so the isolated session can call cron remove.
The polling cron isn't just checking statuses — it's responsible for the FULL mission lifecycle after calls are scheduled. The cron task prompt MUST include instructions to:
update-step to mark steps as in_progress/completed/failed, AND log corresponding eventscompleted (or failed/skipped), then call complete with result_summary and result_payloadIf you skip steps 3-7, the mission will look abandoned in the Telnyx portal — run status stuck on "running", plan steps all "pending", no results recorded. The portal is the source of truth for anyone reviewing missions.
When a call completes and you get a conversation_id, always log it as an event on the mission run. This creates the link between the conversation and the mission in the audit trail:
python telnyx_api.py log-event <mission_id> <run_id> custom \
"Call completed: Miami FL" "batch-calls" \
'{"conversation_id": "conv_xyz", "location": "Miami FL", "phone": "+13052294550"}'
There is no dedicated API to formally link conversations to mission runs. The link-agent command links the assistant, but individual conversations are tracked via events. Always include conversation_id in event payloads so the relationship is traceable.
After scheduling calls, create a cron job like:
Schedule: every 2-3 minutes
Session: isolated (agentTurn)
Task: "Load mission memory from .missions_state.json for slug '<slug>'.
For each call in call_tracker with status 'scheduled' or 'in_progress':
- Poll get-event for the event_id
- If completed with conversation_id: get-insights, update status to 'succeeded', save results
- If failed: increment attempts, schedule retry if under max, or mark abandoned
When ALL calls are succeeded or abandoned:
- Update plan step statuses: update-step for each step (completed/failed)
- Run analysis (compare results)
- Complete the mission (update-step for analyze step, then complete command)
- Report summary to the human
- Remove this cron job"
| Phase | Interval | Rationale | |---|---|---| | Active calls (just scheduled) | Every 2-3 min | Calls complete within minutes | | Waiting for retries | Every 10-15 min | Retries are spaced out | | All calls complete, analyzing | One-shot | Final analysis, then done |
Adjust the interval based on the mission — short batch sweeps poll frequently, long-running outreach can poll less often.
For Class 3 (Sequential Negotiation) and other serial missions, the cron job does more than just poll — it drives the entire call chain. Each poll iteration must:
update-step for the completed call step)dynamic_variablesin_progressresult_summary and result_payloadSchedule: every 2-3 minutes
Session: isolated (agentTurn)
Task: "Load mission state for slug '<slug>'.
Check which call we're on (e.g., call_sequence: 1 of 3).
Poll get-event for the current event_id.
If still in progress: wait for next poll.
If completed:
- Get insights from the conversation
- Update step status: update-step <mid> <rid> 'call-N' completed
- Log event with conversation_id
- Extract negotiation context from insight (e.g., quote, terms)
- If more calls remain:
- Schedule next call with dynamic_variables from extracted context
- Update step status: update-step <mid> <rid> 'call-N+1' in_progress
- Save updated state (increment call_sequence, save new event_id)
- If all calls done:
- Update analyze step, run analysis, complete the mission
- Report summary to human
- Remove this cron job"
Key difference from parallel polling: The cron is stateful — it tracks which call in the sequence is active and drives the chain forward. Save call_sequence, current_event_id, and accumulated results in mission memory so the cron can resume correctly after each firing.
Every mission involving outbound calls needs a retry plan. Calls fail — busy signals, no answer, network issues, IVR timeouts.
Track every number's status in mission memory:
python telnyx_api.py save-memory "<slug>" "call_tracker" '{
"+15551234567": {"status": "pending", "attempts": 0, "location": "Phoenix, AZ"},
"+15559876543": {"status": "pending", "attempts": 0, "location": "Chicago, IL"}
}'
Status values: pending → scheduled → succeeded | failed | no_answer | busy | abandoned
The tracker status should reflect the call_status from the API response:
call_status: completed → tracker status: succeededcall_status: no-answer → tracker status: no_answercall_status: busy → tracker status: busycall_status: failed → tracker status: failedcall_status: canceled → tracker status: abandoned (unless you want to retry)Retry strategy depends on who you're calling. An IVR or restaurant front desk can be called back in minutes. A vendor, interview candidate, or professional contact should get a voicemail and a callback days later during business hours.
Automated systems (IVR, hotlines, info lines):
| Failure Type | Max Retries | Backoff | Notes | |---|---|---|---| | No answer | 3 | 15 min | System may be temporarily busy | | Busy signal | 3 | 10 min | Line may free up quickly | | Network/timeout | 2 | 5 min | Transient, retry sooner | | Empty insights | 2 | 15 min | Adjust DTMF or instructions |
Service industry (restaurants, shops, front desks):
| Failure Type | Max Retries | Backoff | Notes | |---|---|---|---| | No answer | 3 | 30 min - 2 hours | Try different times; avoid peak hours (lunch/dinner for restaurants) | | Busy signal | 3 | 15-30 min | Front desk may be slammed | | Voicemail | 1 | Leave message, retry next business day | Don't leave multiple voicemails | | Empty insights | 2 | 1 hour | May have reached wrong dept |
Professionals (vendors, candidates, B2B contacts):
| Failure Type | Max Retries | Backoff | Notes | |---|---|---|---| | No answer | 2 | 1-2 business days | Enable AMD — leave a voicemail on first no-answer | | Voicemail | 0 | — | Leave one voicemail with callback info, don't call again until they respond or 2+ days pass | | Busy signal | 2 | 2-4 hours | They're on another call | | Empty insights | 1 | Next business day | Retry with clearer instructions |
Key rules for professional contacts:
"voicemail_left": true"Call completed with empty insights" is a distinct failure from "call failed." The call connects but insight extraction returns nothing. Causes:
Consider retrying with adjusted instructions or DTMF sequences.
python telnyx_api.py save-memory "<slug>" "call_tracker" '{"+15551234567": {"status": "no_answer", "attempts": 1, "last_attempt": "2024-12-01T15:00:00Z", "next_retry": "2024-12-01T15:15:00Z"}}'
abandoned and move onpython telnyx_api.py log-event "$MID" "$RID" custom "Retry #2 for Phoenix - no answer on first attempt" "calls" '{"phone": "+15551234567", "attempt": 2, "reason": "no_answer"}'
For missions spanning many numbers, spread retries across time:
When assigning phone numbers to an assistant:
get-available-phone automatically prioritizes these# Automatically prefers HD voice, falls back if unavailable
python telnyx_api.py get-available-phone
# Assign with HD voice enabled (falls back if not supported)
python telnyx_api.py assign-phone <phone_id> <connection_id> voice
# Explicitly disable HD voice if needed
python telnyx_api.py assign-phone <phone_id> <connection_id> voice --no-hd
The Python script provides clear error messages:
Command not working?
python telnyx_api.py check-keyJSON parsing errors?
Missing arguments?
python telnyx_api.py without arguments to see usagetools
Build cross-platform VoIP calling apps with React Native using Telnyx Voice SDK. High-level reactive API with automatic lifecycle management, CallKit/ConnectionService integration, and push notifications. Use for mobile VoIP apps with minimal setup.
tools
Build browser-based VoIP calling apps using Telnyx WebRTC JavaScript SDK. Covers authentication, voice calls, events, debugging, call quality metrics, and AI Agent integration. Use for web-based real-time communication.
tools
Build VoIP calling apps on iOS using Telnyx WebRTC SDK. Covers authentication, making/receiving calls, CallKit integration, PushKit/APNS push notifications, call quality metrics, and AI Agent integration. Use when implementing real-time voice communication on iOS.
tools
Build cross-platform VoIP calling apps with Flutter using Telnyx WebRTC SDK. Covers authentication, making/receiving calls, push notifications (FCM + APNS), call quality metrics, and AI Agent integration. Works on Android, iOS, and Web.