--- name: missions-audit title: "Missions Audit Desk — Plan, Run, and Forensically Trace an Agent Mission" description: "Drive one Telnyx AI mission end-to-end and render a timestamped audit trail of what was planned, what ran, and what failed." language: python framework: vanilla telnyx_products: [AI Missions, AI Assistants, Scheduled Events] --- # Missions Audit Desk — Plan, Run, and Forensically Trace an Agent Mission Drive one Telnyx AI mission end-to-end — create, run, plan steps, log typed events, link the assistant — then render a timestamped audit report of the whole ledger. ## The Story A regional clinic network runs a daily outreach campaign: care coordinators call patients to confirm upcoming appointment slots, and every one of those calls has to be defensible. If a coordinator says a patient was reached, the network needs to show when the call was placed, which assistant handled it, what the outcome was, and who signed off on the plan — because a missed confirmation is a no-show, a no-show is lost revenue, and an undocumented call is a compliance problem the moment a payer asks for the record. The mission is the durable actor: it is born the moment a coordinator commits to an objective ("verify 3 clinic contacts can be reached"), it evolves as each run plans its steps and the worker advances them one by one, and it completes — or terminates as failed — with a result summary a reviewer can replay months later. The mission and its run persist through the ordinary chaos of the job: the worker script crashing mid-campaign, the scheduled call sitting in `in_progress` longer than expected, an operator pausing a run to re-check a number. Every state change the worker observes is mirrored into the ledger, so the record reflects the world, not the worker's self-report. The rest of this README is the API surface of that story. ## Why Telnyx Telnyx is **AI Communications Infrastructure** — the mission ledger, the AI assistant, and the scheduled call all live on one platform, so the audit trail is a first-class API resource rather than something you bolt on afterwards. The Missions API records what was planned, what ran, and what failed as durable, queryable objects; the assistant that places the call is the same assistant you link back to the run. There is no separate observability vendor, no log-scraping glue, and no gap between "the call happened" and "the call is documented." ## Telnyx API Endpoints Used | Method | Endpoint | Purpose | |--------|----------|---------| | `POST` | `/ai/missions` | Create the durable mission (`name` required) | | `GET` | `/ai/missions` | List missions (paginated) | | `GET` | `/ai/missions/{mission_id}` | Mission detail | | `POST` | `/ai/missions/{mission_id}/runs` | Start one execution of the mission | | `GET` | `/ai/missions/{mission_id}/runs` | List runs for a mission | | `GET` | `/ai/missions/{mission_id}/runs/{run_id}` | Run detail — `RunStatus`, `result_summary`, `result_payload` | | `PATCH` | `/ai/missions/{mission_id}/runs/{run_id}` | Update run status / `result_summary` / `result_payload` | | `POST` | `/ai/missions/{mission_id}/runs/{run_id}/plan` | Submit the initial plan | | `GET` | `/ai/missions/{mission_id}/runs/{run_id}/plan` | Fetch the planned steps | | `PATCH` | `/ai/missions/{mission_id}/runs/{run_id}/plan/steps/{step_id}` | Advance a step status | | `POST` | `/ai/missions/{mission_id}/runs/{run_id}/events` | Log a typed event (`type` + `summary` required) | | `GET` | `/ai/missions/{mission_id}/runs/{run_id}/events` | List events (the audit timeline) | | `POST` | `/ai/missions/{mission_id}/runs/{run_id}/telnyx-agents` | Link the assistant to the run | | `GET` | `/ai/missions/{mission_id}/runs/{run_id}/telnyx-agents` | List linked agents | | `POST` | `/ai/assistants` | Create the demo outreach assistant | | `POST` | `/ai/assistants/{assistant_id}/scheduled_events` | Schedule the outreach call | | `GET` | `/ai/assistants/{assistant_id}/scheduled_events/{event_id}` | Poll the scheduled event to terminal status | The Missions surface is invoked via raw REST against `TELNYX_API_BASE` (default `https://api.telnyx.com/v2`) with `Authorization: Bearer $TELNYX_API_KEY`, unwrapping the `{"data": ...}` envelope. The Assistants and scheduled-events endpoints return the object **bare** (no envelope) — see `API.md` for the full typed contract. ## Architecture The audit desk is a small loop around one durable objective. The **mission** is the durable objective — the thing that persists across runs. A **run** is one execution of it, and each run carries a **plan** whose step statuses the executor advances as work happens. The **worker** (`execute.py`) is the actor doing real work: creating the assistant, creating the scheduled event, watching it reach terminal status. Its contract is honesty — it logs every state change it observes, not only its own actions. The **audit renderer** (`audit.py`) pulls mission, run, plan, events, and agent linkage into one chronological report. ```text create_mission.py start_run.py plan_steps.py ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ POST │ │ POST │ │ POST │ │ /ai/missions │───────▶│ .../runs │─────────▶│ .../runs/{rid}/ │ │ │ │ │ │ plan │ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │ mission_id │ run_id │ step_ids ▼ ▼ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ execute.py (WORKER) │ │ │ │ step 1: provision assistant ──▶ POST /ai/assistants │ │ step 2: schedule call ──▶ POST .../scheduled_events │ │ step 3: verify outcome ──▶ GET .../scheduled_events/{id} │ │ │ │ after every unit of work: │ │ PATCH .../plan/steps/{step_id} (pending→in_progress→…) │ │ POST .../events (step_started/step_completed/…) │ │ PATCH .../runs/{run_id} (running → succeeded | failed) │ │ POST .../telnyx-agents (link the assistant to the run) │ └───────────────────────────────┬──────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────────┐ │ audit.py (RENDERER) │ │ │ │ poll GET .../runs/{run_id} until terminal (cap 10 min) │ │ fresh GET run / plan / events / telnyx-agents (cross-check) │ │ │ │ ──▶ MISSIONS AUDIT REPORT (timestamped, in order) │ └──────────────────────────────────────────────────────────────────┘ ``` **RunStatus enum** (walked by the demo: `pending` → `running` → `succeeded` | `failed`): | Status | Meaning | |--------|---------| | `pending` | Run created, no work started. | | `running` | Worker is executing steps. | | `paused` | Run suspended; resume via `POST .../runs/{run_id}/resume`. | | `succeeded` | All steps completed. | | `failed` | A step failed; see `result_summary`. | | `cancelled` | Run cancelled via `POST .../runs/{run_id}/cancel`. | **StepStatus enum** (walked by the demo: `pending` → `in_progress` → `completed` | `failed`; `skipped` is documented but unused): | Status | Meaning | |--------|---------| | `pending` | Planned, not started. | | `in_progress` | Worker is executing this step. | | `completed` | Step finished successfully. | | `skipped` | Step intentionally not run. | | `failed` | Step raised an error. | **EventType enum** (logged by the worker): `status_change`, `step_started`, `step_completed`, `step_failed`, `tool_call`, `tool_result`, `message`, `error`, `custom`. The worker logs `step_started` / `step_completed` / `step_failed` per step transition (with the `step_id` field set), one `custom` event per call attempt with `payload` `{"scheduled_event_id", "call_status", "telnyx_call_control_id", "attempt_number"}`, one `status_change` event per observed scheduled-event transition, and `error` events for poll timeouts or linkage failures. **Linked-agents response shape** (`TelnyxAgentData`): `run_id`, `telnyx_agent_id`, `created_at`. ## Environment Variables | Variable | Type | Example | Required | Description | Where to get it | |----------|------|---------|----------|-------------|-----------------| | `TELNYX_API_KEY` | `string` | `your_telnyx_api_key_here` | yes | Telnyx API key used for all Bearer-authenticated calls. | Telnyx Portal → API Keys | | `TELNYX_API_BASE` | `string` | `https://api.telnyx.com/v2` | no | Telnyx API base URL. Override only for a non-production environment. | — | | `DEMO_MODE` | `string` | `true` | no | `true` (default) walks the missions ledger with no assistant, no scheduled event, and no call. `false` runs the live campaign. | — | | `DESTINATION_NUMBER` | `string` | `+15550001234` | yes (live) | The contact number the outreach call is placed to (E.164, validated by `execute.py`). | Your CRM / test number | | `ASSISTANT_PHONE_NUMBER` | `string` | `+15550001234` | yes (live) | Phone number associated with the assistant in the Portal; used as `telnyx_agent_target` on the scheduled event. | Telnyx Portal → AI → Assistants → Phone Numbers | | `ASSISTANT_ID` | `string` | `assistant-1234abcd` | no | Reuse an existing assistant instead of creating one (live mode). | Output of a previous run, or Telnyx Portal | | `MISSION_ID` | `string` | `your_mission_id_here` | no | Mission id from `create_mission.py`; used as the default by later scripts. | Output of `create_mission.py` | | `RUN_ID` | `string` | `your_run_id_here` | no | Run id from `start_run.py`; used as the default by later scripts. | Output of `start_run.py` | > **Agent / CLI access** — provision and inspect missions programmatically with the [Telnyx CLI](https://developers.telnyx.com/development/cli): > > ```bash > telnyx auth login > telnyx ai:missions create --name "Clinic outreach audit" > telnyx ai:missions list > ``` > > Phone numbers and assistants are provisioned in the Telnyx Portal (AI → Assistants → Phone Numbers). Full API discovery: [llms-full.txt](https://developers.telnyx.com/llms-full.txt) · [CLI docs](https://developers.telnyx.com/development/cli) ## Setup ```bash git clone https://github.com/team-telnyx/telnyx-code-examples.git cd telnyx-code-examples/missions-audit python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt cp .env.example .env # edit .env: set TELNYX_API_KEY (all other vars are optional for demo mode) ```
Programmatic / CLI setup ```bash # Install CLI — https://developers.telnyx.com/development/cli go install github.com/team-telnyx/telnyx-cli/cmd/telnyx@latest telnyx auth login # Inspect existing missions (create_mission.py creates the mission itself) telnyx ai:missions list ``` For full API discovery, point your agent at [`llms-full.txt`](https://developers.telnyx.com/llms-full.txt).
The workflow is five standalone scripts, run in order. Each one prints the ids the next one needs. | # | Script | Role | Command | |---|--------|------|---------| | 1 | `create_mission.py` | Create the mission via `POST /ai/missions`, print `mission_id`, list missions. | `python create_mission.py` | | 2 | `start_run.py` | Start a run via `POST /ai/missions/{mission_id}/runs`, print `run_id` and initial status. | `python start_run.py --mission-id ` | | 3 | `plan_steps.py` | Submit the initial 3-step plan via `POST .../runs/{run_id}/plan`, print step ids. | `python plan_steps.py --mission-id --run-id ` | | 4 | `execute.py` | Run the campaign: advance each step, log events; in live mode create the assistant, schedule the call, poll it, link the assistant. | `python execute.py --mission-id --run-id ` | | 5 | `audit.py` | Wait for terminal status, cross-check with a fresh `GET`, render the audit report. | `python audit.py --mission-id --run-id ` | `app.py` is a thin help-dispatcher — it prints the menu and points you at the right script: ```bash python app.py --list ``` Run the smoke test to confirm every module imports and the enums/plan are intact: ```bash pytest smoke_test.py -q ``` ### Demo mode vs live mode `DEMO_MODE=true` (the default) is safe by default: `execute.py` walks the full missions ledger — run status, all three steps, step events, the terminal run update — but creates **no assistant**, creates **no scheduled event**, places **no call**, and performs **no agent linkage**. Every event it logs describes the simulated walk (it never records a call status or scheduled-event id that does not exist), and the run finishes `succeeded` with an honest `result_summary` saying exactly that. Set `DEMO_MODE=false` in `.env` to place the real scheduled call through the assistant. Live mode requires `DESTINATION_NUMBER` (validated as E.164) and `ASSISTANT_PHONE_NUMBER`; the script exits with an actionable error if either is missing. ### Looping the plan for multiple contacts The demo places **one** call — the "3 contacts" objective is the mission description string only, and the 3-step plan executes once so the audit ledger stays focused on the trail mechanics. To cover multiple contacts, wrap the `execute.py` step loop in a `for` over your contact list, submitting a fresh plan per contact (or one plan with a step per contact) and logging one `custom` event per attempt with an incrementing `attempt_number`. ## API Reference Each script exposes a CLI and a small set of importable functions. Full request/response shapes are in `API.md`. ### `create_mission.py` ```text python create_mission.py [--name TEXT] [--description TEXT] [--instructions TEXT] ``` | Function | Signature | Endpoint | |----------|-----------|----------| | `create_mission` | `(name, description=None, instructions=None, execution_mode="external") -> dict` | `POST /ai/missions` | | `list_missions` | `(limit: int = 5) -> list[dict]` | `GET /ai/missions` | ### `start_run.py` ```text python start_run.py [--mission-id TEXT] ``` | Function | Signature | Endpoint | |----------|-----------|----------| | `start_run` | `(mission_id: str) -> dict` | `POST /ai/missions/{mission_id}/runs` | | `list_runs` | `(mission_id: str) -> list[dict]` | `GET /ai/missions/{mission_id}/runs` | ### `plan_steps.py` ```text python plan_steps.py [--mission-id TEXT] [--run-id TEXT] ``` | Function | Signature | Endpoint | |----------|-----------|----------| | `create_plan` | `(mission_id, run_id, steps=None) -> list[dict]` | `POST /ai/missions/{mission_id}/runs/{run_id}/plan` | | `get_plan` | `(mission_id, run_id) -> list[dict]` | `GET .../runs/{run_id}/plan` | Default plan: `provision_assistant` → `schedule_outreach_call` → `verify_outcome` (sequences 1–3). ### `execute.py` ```text python execute.py [--mission-id TEXT] [--run-id TEXT] ``` | Function | Signature | Endpoint | |----------|-----------|----------| | `update_step` | `(mission_id, run_id, step_id, status) -> dict` | `PATCH .../plan/steps/{step_id}` | | `log_event` | `(mission_id, run_id, event_type, summary, step_id=None, payload=None) -> dict` | `POST .../events` | | `update_run` | `(mission_id, run_id, status=None, summary=None, payload=None) -> dict` | `PATCH .../runs/{run_id}` | | `link_agent` | `(mission_id, run_id, assistant_id) -> dict` | `POST .../telnyx-agents` | | `list_linked_agents` | `(mission_id, run_id) -> list[dict]` | `GET .../telnyx-agents` | | `create_assistant` | `() -> str` | `POST /ai/assistants` | | `create_scheduled_event` | `(assistant_id, agent_target, end_user_target) -> dict` | `POST .../scheduled_events` | | `get_scheduled_event` | `(assistant_id, event_id) -> dict` | `GET .../scheduled_events/{event_id}` | The worker polls `GET /ai/assistants/{assistant_id}/scheduled_events/{event_id}` every 5 seconds with a hard cap of 10 minutes, mirroring every observed status change as a `status_change` missions event. A poll timeout is logged as an `error` event and fails the verify step — the ledger never claims a call outcome the platform did not report. ### `audit.py` ```text python audit.py [--mission-id TEXT] [--run-id TEXT] [--nowait] ``` | Function | Signature | Endpoint | |----------|-----------|----------| | `wait_for_terminal` | `(mission_id, run_id) -> tuple[dict, bool]` | `GET .../runs/{run_id}` (polled) | Renders the mission, run, per-step planned-vs-completed table, the full event timeline with `EventType` values, the linked agent rows, and both status enums. Exits `0` on a terminal run, `1` otherwise (and warns if the 10-minute poll cap was reached). ## Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| | `TELNYX_API_KEY is not set` | `.env` missing or not loaded | `cp .env.example .env` and set `TELNYX_API_KEY` | | `422 ... name: field required` on create | Mission create payload missing `name` | `create_mission.py` sends `name` by default; pass `--name` if you overrode it | | `ASSISTANT_PHONE_NUMBER is not set` | Live mode with no portal-associated number | Telnyx Portal → AI → Assistants → Phone Numbers, associate a number, then set `ASSISTANT_PHONE_NUMBER` in `.env` | | `DESTINATION_NUMBER ... is not a valid E.164 number` | Bad destination format (live mode) | Set an E.164 number, e.g. `+15550001234` | | `expected 3 planned steps; run plan_steps.py first` | `execute.py` run before the plan was submitted | Run `plan_steps.py` with the same `--mission-id` / `--run-id` | | `404 Not Found` on step updates | Step endpoints miss the `/plan/` segment | Steps live under `.../runs/{run_id}/plan/steps/{step_id}` — the scripts already do this | | Audit report shows `status: running` | Run never reached terminal within the 10-minute cap | Check the scheduled event in the Portal; re-run `audit.py` (or with `--nowait`) | | Step 3 fails with `scheduled event ended as failed` | The scheduled call failed | The run is marked `failed` with a `result_summary` naming the failing step — inspect the event timeline | | `401 Unauthorized` from any endpoint | Bad or expired API key | Regenerate the key in the Portal and update `.env` | | `429 Too Many Requests` | Polling too aggressively | The scripts already poll at 5s intervals; reduce concurrency | ## Agent Discovery This example is part of the [Telnyx Code Examples](https://github.com/team-telnyx/telnyx-code-examples) catalog. - **Agent signup**: [telnyx.com/agent-signup.md](https://telnyx.com/agent-signup.md) — automated account provisioning via agent mail; get an API key with no human intervention - **Agent CLI**: [github.com/team-telnyx/ai/tree/main/cli](https://github.com/team-telnyx/ai/tree/main/cli) — composite commands for agents ([commands reference](https://github.com/team-telnyx/ai/tree/main/cli/src/commands)) - **Agent skills**: [github.com/team-telnyx/ai/tree/main/skills](https://github.com/team-telnyx/ai/tree/main/skills) - **Telnyx AI repo**: [github.com/team-telnyx/ai](https://github.com/team-telnyx/ai) - **LLM-optimized docs**: [`llms-full.txt`](https://developers.telnyx.com/llms-full.txt) - **Example index**: [`llms.txt`](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/llms.txt) - **Telnyx CLI (human)**: [developers.telnyx.com/development/cli](https://developers.telnyx.com/development/cli) — `go install github.com/team-telnyx/telnyx-cli/cmd/telnyx@latest` ## Related Examples - [Missions AI Task Runner](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/missions-ai-task-runner-python/README.md) — AI-driven task execution within the Telnyx Missions framework - [Missions Workflow Orchestrator](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/missions-workflow-orchestrator-python/README.md) — create and manage multi-step mission workflows ## Resources - [Voice outreach with AI missions](https://developers.telnyx.com/docs/inference/missions) - [Missions API reference index](https://developers.telnyx.com/api-reference/missions) - [Create mission](https://developers.telnyx.com/api-reference/missions/create-mission) - [Start a run](https://developers.telnyx.com/api-reference/missions/start-a-run) - [List runs for mission](https://developers.telnyx.com/api-reference/missions/list-runs-for-mission) - [Create initial plan](https://developers.telnyx.com/api-reference/missions/create-initial-plan) - [Update run](https://developers.telnyx.com/api-reference/missions/update-run) - [Update step status](https://developers.telnyx.com/api-reference/missions/update-step-status) - [Log event](https://developers.telnyx.com/api-reference/missions/log-event) - [List linked Telnyx agents](https://developers.telnyx.com/api-reference/missions/list-linked-telnyx-agents) - [Scheduled events for AI assistants](https://developers.telnyx.com/api-reference/assistants/create-a-scheduled-event) - [Telnyx Python SDK](https://developers.telnyx.com/development/sdk/python) - [AI Assistants product page](https://telnyx.com/ai-assistants) - [Telnyx pricing](https://telnyx.com/pricing)