After-hours chat becomes an operational problem as soon as a conversation must survive more than one request.
The application needs to remember what the person already reported, continue the same AI conversation, avoid repeatedly alerting the on-call team, and produce a useful handoff the next morning. A stateless chatbot does not provide those guarantees by itself.
Night Nurse Line is an open-source TypeScript sample that demonstrates this pattern with Telnyx AI Assistants, Stateful Actors, Conversations, Messaging, and SQL.
Important: This is an educational communications workflow, not medical advice, a diagnostic system, or a production clinical protocol. The model-generated triage label must not replace clinician-approved rules or emergency escalation. A healthcare deployment also needs appropriate privacy, security, consent, access-control, retention, audit, and regulatory review.
What the sample builds
The example creates one durable NightNurse actor per patient handle. That actor owns:
- The Telnyx conversation ID used across turns.
- Extracted symptoms, medications, allergies, and pregnancy-week text.
- The most recent triage label returned by the assistant.
- A
pageSentguard for the current actor state. - Counters used by the reporting workflow.
The wider application also includes:
- A browser relay that sends chat turns through the Edge function.
- A text-only Telnyx web-chat widget.
- An Ed25519-verified messaging webhook for rota changes and staff replies.
- A
RotaDeskactor containing the current on-call destination. - A
ClinicDeskactor that produces and texts a daily handoff summary.
The architecture looks like this:
Browser relay Telnyx web-chat widget | | POST /chat POST /conversation | | +------------+---------------------+ | NightNurse actor for patientId |-- durable conversation ID |-- chart-like context |-- pageSent guard |-- SQL session ledger | +----------+-----------+ | | Assistant Chat API RotaDesk actor | Telnyx Messaging ClinicDesk actor -> daily SQL report -> morning handoff SMS One actor per patient handle
The router derives the same actor identity for every request carrying the same patient handle:
const id = env.NURSES.idFromName(patientId); const nurse = env.NURSES.get(id); This makes the actor the durable owner of that conversation state. A new request does not have to reconstruct the thread from browser memory, and a recycled application process does not erase the stored actor state.
The relay path posts each patient turn to POST /chat:
{ "patientId": "patient-demo-481", "text": "I need help understanding what to do next.", "conversationId": "optional-existing-conversation-id" } Use pseudonymous handles in testing. Do not place protected health information into logs, URLs, screenshots, or demo fixtures.
The actor reuses its stored conversationId when it calls the Assistant Chat API. That preserves the assistant's conversation thread while the actor separately maintains application state and SQL records.
Turning an assistant response into application policy
The sample configures the assistant to end every response with one machine-readable line:
TRIAGE: self-care TRIAGE: appointment TRIAGE: page-on-call The actor removes that suffix from the user-facing response and parses the label:
const TRIAGE_RE = /TRIAGE:\s*(self-care|appointment|page-on-call)\s*$/i; If the suffix is missing, the current sample defaults to appointment. That keeps malformed output from silently becoming a page, but it is only a demo fallback. Production behavior should be defined by clinician-approved policy, tested against representative cases, and fail toward an explicit human or emergency path when output is unavailable or uncertain.
The application also extracts a small set of facts from the raw patient text and writes them to SQL. This is useful for demonstrating durable context, but regex extraction is not a validated clinical record or a substitute for an electronic health record integration.
Guarding the on-call page attempt
When the parsed label is page-on-call, the actor checks its durable pageSent flag:
let pageSent = state.pageSent; if (redFlag && !state.pageSent) { pageSent = true; await this.schedule(0, "pageOnCall", { patientId, summary: buildPageSummary(patientId, input.text, facts), }); } The scheduled method asks RotaDesk for the active destination and attempts to send an SMS through the Telnyx binding:
async pageOnCall(payload) { const desk = this.env.ROTAS.idFromName("oncall"); const number = await desk.current(); await sendSms(this.env, number, `Night Nurse page: ${payload.summary}`); } This guard prevents later turns from intentionally scheduling another page after the flag is stored. It should not be described as guaranteed exactly-once delivery.
There are two important production boundaries:
- A process interruption between scheduling the page task and persisting
pageSentcan allow a replay to schedule another task. - If the SMS attempt fails after
pageSentis stored, the flag can suppress a retry even though the page was not delivered.
A production design needs a durable outbox or idempotency key, explicit delivery-state reconciliation, retries with limits, and escalation when delivery cannot be confirmed. For urgent clinical workflows, SMS alone should not be treated as proof that a clinician received or accepted the case.
Two chat paths, one durable state owner
The project demonstrates two browser experiences.
Relay page
The relay page sends each completed patient turn to the Edge worker. The worker calls the Assistant Chat API, parses the result, updates the actor, and returns the assistant's response.
This path gives the application direct control over each turn.
Telnyx web-chat widget
The widget talks to Telnyx directly without placing an API key in browser code. A bridge forwards the conversation ID and transcript events to POST /conversation.
The actor then reads conversation messages, processes only new items, updates the same chart-like context, and applies the same page guard.
The two paths demonstrate why the actor, rather than the browser, should own operational state.
Changing the on-call rota over SMS
Clinic staff can send a command such as:
ONCALL <ON_CALL_E164> The /sms route verifies the Telnyx webhook signature in live mode before updating the RotaDesk actor. Another command routes a staff message into a named patient thread:
PATIENT <PATIENT_HANDLE> <MESSAGE> In a production system, signature verification is only the first control. Add sender authorization, role checks, audit history, command confirmation, and a policy for lost or reassigned staff phones.
Producing the morning handoff
ClinicDesk registers a daily schedule and queries SQL for the current reporting day. The report includes counts for chats, page attempts, self-care labels, and appointment labels.
When DAY_TEAM_E164 is configured, the application sends that summary by SMS. This gives the next shift an operational view without scanning every conversation manually.
The same reporting pattern can support other use cases, such as after-hours property management, veterinary intake, IT incident intake, or field-service escalation. Replace the medical labels and chart fields with domain-specific, reviewed policy.
Run it safely in demo mode
The project starts with DEMO_MODE=true. In this mode, actor state and SQL operations run normally, while SMS actions are logged with masked destinations instead of being sent.
git clone https://github.com/team-telnyx/telnyx-code-examples.git cd telnyx-code-examples/night-nurse-line npm install cp .env.example .env npm run smoke Create the assistant and deploy after configuring your own test environment:
bash scripts/setup_assistant.sh telnyx-edge auth api-key set "$TELNYX_API_KEY" telnyx-edge types telnyx-edge ship Keep all destinations as placeholders until you are ready to use account-owned test numbers. Set DEMO_MODE=false only after configuring the sender, webhook public key, authorized operators, and delivery monitoring.
The Telnyx Agent SDK documentation currently labels the SDK as beta. Review the current surface and release notes before treating this sample's runtime interfaces as stable production contracts.
Production checklist
Before adapting this pattern to a real healthcare workflow, add at least:
- Clinician-authored triage and emergency-routing policy.
- A visible instruction to contact emergency services for emergencies.
- Human review and escalation for uncertain or malformed model output.
- Delivery receipts, retries, acknowledgment, and fallback channels.
- Authentication and authorization for staff commands and patient access.
- Encryption, minimum-necessary data handling, retention, and audit controls.
- Evaluation for unsafe advice, missed escalation, and repeated escalation.
- Legal, privacy, security, and regulatory review appropriate to the deployment.
The reusable lesson is not that an LLM should act as a nurse. It is that conversational applications need a durable owner for context, actions, and handoff state after the current HTTP request or browser session disappears.
