healthcare

Hospital Appointments

by Hospital Appointments

Phone appointment booking for a hospital. The AI receptionist looks callers up by number, then books, reschedules or cancels against real availability — and every change lands in the CRM straight away.

healthcareNative

Reviewed & approved · install free · connect your tools

1
Installs
1.0.0
Version
9
Tools

Last updated 2026-07-31

Screenshots

Hospital Appointments screenshot 1

About Hospital Appointments

Hospital Appointments

Phone appointment booking for a hospital, on the Telenow app platform.

One AI receptionist answers the line, looks the caller up by number, and books, reschedules or cancels against real availability — in Hindi or English, whichever the caller speaks, though the CRM itself is always written in English underneath. Every change lands in the CRM immediately. A second, outbound agent can call a patient back about one specific appointment. A small three-page dashboard sits behind it for reception staff.

Zero hosting. Every tool is a declarative handler running on Telenow's own runtime — there is no server to deploy and nothing that can be down at 2am.


What it is

5 objectsdoctor, patient, slot, appointment, department
9 toolsthe ones the agent calls mid-call
2 agentsthe inbound receptionist — 18 nodes, 49 edges — and an outbound reminder caller (AGENT.md)
3 pagesAppointments · Patients · Doctors & diary
1 workflowmarks a slot booked the moment an appointment is created
8 scopesfive objects:*, agents:read, calls:initiate, softphone:dial

The call

"May I have your mobile number?"
        │
   find_patient(phone)  +  find_appointment(phone, scheduled)
        │
   ┌────┴──────────────────┬──────────────────────────┐
   │ known, has a booking  │ known, nothing booked    │ not on the system
   │                       │                          │
   ├─ book another ──┐     │ "Hello Asha — I can see  ├─ name, DOB, region
   ├─ reschedule ────┤     │  you last saw Dr Rao"    │
   ├─ cancel         │     │                          │
   └─ hear details   │     │ (no re-registration)     │
                     ▼     ▼                          ▼
              list_doctors  →  find_available_slots
                     │                │
              clash? offer alternatives, or change doctor
                     │
              read the summary back → explicit yes
                     │
              create_patient (if new) + book_appointment
                     │
              read back the reference → CRM updated

The 9 tools

ToolHandlerWhat it does
find_patientobject.queryWho is calling, and their visit history. Called first, every call.
find_appointmentobject.queryWhat they have booked. Called alongside find_patient.
get_appointmentobject.queryFetch one booking by its reference.
list_doctorsobject.queryThe only source of doctors the agent may name.
find_available_slotsobject.queryThe only source of times. Never promises before checking.
create_patientobject.createOnly for callers with no record, and only at confirmation.
book_appointmentobject.createWrites the booking. Fires the slot-locking workflow.
reschedule_appointmentobject.updateSame reference, new time. Matches on ref.
cancel_appointmentobject.updateOnly after an explicit yes. Matches on ref.

Call-back: a human dial-out, and a second AI agent

The Patients and Appointments pages both carry two buttons per row:

  • Call opens the dashboard's own softphone, prefilled with the patient's number, for a hospital staff member to dial themselves. Needs the softphone:dial scope.
  • Remind places a real outbound call from the Hospital Reminder Caller agent (manifest/reminder-agent.mjs) — it reads the patient their appointment and can reschedule or cancel it right there, using the exact same reschedule_appointment/cancel_appointment tools as the receptionist. Needs the calls:initiate scope, and the agent to have been built once from the Doctors page.

Both are genuine platform capabilities (telenow.softphone.dial / telenow.calls.initiate), not a "callback requested" flag — see AGENT.md for how the reminder-caller flow is put together and why it reuses reschedule_appointment for a post-cancel rebook rather than a second booking path.


Design decisions worth knowing

Availability is materialised, not calculated. A voice agent has no compute — object.query filters by equality on declared fields, returns 50 rows, and cannot sort. So "what's free on Tuesday with Dr Rao?" can only be answered deterministically if the answer already exists as rows. Each doctor's working pattern is expanded into slot rows, and find_available_slots becomes a lookup.

The cost, stated plainly: the diary is only as fresh as the last time the CRM was opened. Two things keep that honest. Past slots are expired on load, so a stale diary returns nothing rather than yesterday's times — failing closed beats an agent offering an appointment that already happened. And the Doctors page shows exactly how much diary is left.

Booking is automated; cancelling is reconciled. Only a CREATE fires object.<type>.created on this platform — a PATCH and a DELETE fire nothing at all. So booking locks its slot via a workflow, instantly and server-side. Cancelling and rescheduling are updates, so no workflow can see them; the CRM frees those slots when it loads (reconcileSlots in ui/lib/slots.ts). Anyone opening the CRM fixes it for everyone, and genuine double-bookings are surfaced rather than silently resolved.

The caller is looked up as a person, not just as a booking. find_patient and find_appointment are separate calls because a patient seen last year with nothing booked has no appointment to find — but is very much an existing patient. Searching appointments alone would ask them for their name and date of birth all over again and create a second record for them, splitting their history and making the next lookup ambiguous. Visit history lives denormalised on the patient row (last_visit_date, last_doctor_name, visit_count) because object.query cannot sort, count or aggregate: the agent has no way to work out "when did I last come in?" from raw appointment rows mid-call, so the answer is written when it changes and simply read back.

The booking reference is pre-minted on the slot. An LLM asked to invent a unique code will collide with an existing one eventually, and two patients sharing a reference is unrecoverable over the phone. Every slot carries a booking_code; the agent copies it onto the appointment as ref.

Never use window.confirm / window.prompt / window.alert in this app. The dashboard renders every app inside a sandboxed iframe, and native browser dialogs are blocked there unless the sandbox carries allow-modals — which it does not. The failure is silent: confirm() returns false and prompt() returns null without showing anything, so if (!confirm(...)) return simply never proceeds and the button looks dead. Worse, it works under npm run dev, because the harness runs top-level rather than in an iframe — so this class of bug survives local testing completely. Use useConfirm() from ui/components/ui.tsx instead; it renders a real in-page dialog and supports an optional text field for things like a cancellation reason.

The agent writes the country code, because nothing downstream can. Patients say ten digits and should never be asked for a +91. But the agent's tools are declarative object.create / object.query handlers: the runtime stores exactly what the model sends and matches by exact string equality, with no normalisation step anywhere. A patient registered as 9876500001 is therefore invisible to the next lookup for +919876500001. So the agent is told — in the standing rules, on the greet node, and on every phone parameter it fills — to write every number as exactly +91 plus the ten digits, and nothing else. One form with zero branches, because a rule with an exception ("keep a foreign code if they said one") is a branch the model will eventually take wrongly mid-call. Because an instruction is not a guarantee, healPhoneNumbers in ui/lib/heal.ts rewrites anything already stored the wrong way when the CRM loads, patients and appointments together so the two never come unlinked.

The CRM never stores Hindi, even though the agent speaks it. A caller may talk in Hindi, English, or a mix, and the agent mirrors whichever they use. But every value written into a tool call — a name, a region, a reason — is translated into English first, and the same rule applies to searching: the agent translates what it heard before calling find_patient, rather than matching on the caller's own words. This is what stops the obvious failure mode of an agent that stores a name in Devanagari and then can't find that same patient on the next call because it's now searching in English. doctor.department gets the analogous fix on the CRM side: it is picked from a managed catalog (the department object) rather than typed free text, so it can't drift into two spellings of the same department across doctors.


Testing the UI without a dashboard

npm run dev

A working hospital at http://localhost:5174 — no Telenow account, no install, no phone number. Four doctors, a fortnight of diary, ~126 appointments, all dated from the day you run it. That is why telenow.dev.json is generated rather than committed: a static fixture is correct for one day and then shows an empty hospital, which reads as a bug in the app.

The harness adds three things createMockBridge does not provide:

  • The design system. --tn-* variables and .tn-* classes are injected by the dashboard; standalone the CRM would render as unstyled HTML. A faithful stand-in plus a light/dark toggle. Treat colour and spacing as approximate and confirm in Dev preview.
  • Realtime. The mock's data.subscribe is a no-op, so nothing re-lists after a write and the UI looks frozen.
  • Fixture sync. The mock seeds localStorage once and ignores the file forever after. A stamp makes npm run mock take effect automatically.

Bottom-right: light / dark, and reset data.

telenow dev reads telenow.dev.json once, at startup, and embeds it in the page. So running npm run mock while the server is up changes nothing on screen — restart the dev server to pick up new fixtures. npm run dev does both in the right order.

None of this ships. npm run dev generates a manifest whose ui.entry points at ui/dev/index.tsx; npm run build always regenerates with the real entry. Excluded by construction, not guarded by a runtime flag.

For real data use the dashboard's Dev preview and paste the localhost URL — but every bridge call there is a real write against the installing organisation, so use a test org.


Development

npm run manifest    # compose telenow.app.json from manifest/ and lint it
npm run mock        # regenerate telenow.dev.json, dated from today
npm test            # 46 mutation tests proving the linter catches real bugs
npm run typecheck   # tsc over the UI
npm run dev         # standalone hospital with mock data
npm run build       # validate, bundle, zip

The linter in scripts/lint-manifest.mjs is deliberately stricter than telenow validate, which checks only a subset. Every rule targets a failure mode that passes validation, uploads cleanly, and then breaks silently on a live call — a flow whose start node does not exist (the agent quietly degrades to single-context), an equation edge missing its {} braces (parses false, never fires), an object.update matching an undeclared field, an icon off the whitelist. npm test corrupts the manifest 46 ways and asserts each rule fires, because a linter nobody has seen fail is indistinguishable from one that returns nothing.

manifest/
  index.mjs         app id, scopes, assembly
  objects.mjs       the 5 objects
  tools.mjs         the 9 tools
  agent.mjs         the receptionist's flow graph + SHARED_PATIENT_RULES
  reminder-agent.mjs  the outbound reminder caller's flow graph
  automation.mjs    the slot-locking workflow
  settings.mjs      per-install settings
  ui.mjs            the 3 pages
ui/
  pages/            Appointments · Patients · Doctors
  lib/slots.ts      generate, expire, reconcile
  lib/heal.ts       rewrite non-canonical phone numbers on load
  dev/              harness — never bundled into a release
scripts/
  build-manifest.mjs · build-fixtures.mjs · lint-manifest.mjs · lint.test.mjs

Changing the app id (it is global and first-come, so upload can 409):

npm run set-app-id -- your-new-id

Do not edit it by hand — the id also appears inside flow tool nodes, and a mismatch means the node is silently stripped at install.

Ready-made agents it ships

Hospital Appointment Receptionistmulti-step flow

Answers the hospital appointment line. Looks the caller up by phone number, then books, reschedules or cancels — checking real availability before offering any time, and writing every change straight to the CRM.

Hospital Reminder Callermulti-step flow

Calls a patient back about one specific appointment (triggered from the Patients or Appointments page) — reads it out, and can reschedule or cancel it right there on the call.

What your agents can do with it

find_patient

Look up who is calling, by their mobile number. Call this at the very start of every call, together with find_appointment. If a record comes back, greet them by their first name and DO NOT ask for their name, date of birth or region again — you already have them. The record also tells you when they last came in and which doctor they saw, which you may mention. If nothing comes back they are new to the hospital and you will need to register them. Never read a date of birth out loud; ask them for it if you need to check it. If you pass a name, it must be in English (translated, not transliterated) even if the caller spoke Hindi — the CRM never stores Hindi, so a Hindi-script name will not match an existing record.

find_appointment

Look up a caller's appointments by their mobile number. Call this at the start of every call, alongside find_patient. Pass status "scheduled" to see what they have COMING UP — that is what you almost always want, and it stops you reading out an appointment that has already happened as though it were still to come. Leave status out only when the caller asks about a past visit. If results is empty they have nothing booked. Never describe an appointment you did not get from this tool. The phone number is the only match key — it does not need translating, but any name you mention alongside it in conversation still must be in English, never Hindi script.

get_appointment

Fetch a single appointment by its booking reference, for when the caller quotes one such as "APT-4821", or when you need to re-read the exact details of a booking before changing it. Prefer find_appointment when you only have a phone number.

list_doctors

List the doctors the caller can be booked with. Call this before offering any doctor by name — you may only ever mention a doctor that came back from this tool. Use it when the caller asks who is available, names a department such as Cardiology or Paediatrics, or has no preference and needs options. Read back the title, name and department. Never read the doctor_code out loud.

find_available_slots

Get the genuinely free appointment times for a doctor. You MUST call this before offering or confirming any time — never promise a slot you have not checked. Pass doctor_code plus either date as YYYY-MM-DD, or day_bucket if the caller was vague. Each result carries date_label and time_label already written out for speech: read those, never the raw timestamp and never the slot_ref. Offer at most three times at once. If results is empty, that day is fully booked — say so plainly and offer another day, or offer to try a different doctor.

create_patient

Create a record for a caller who has no appointment history. Call this ONCE, only after you have collected and read back their full name, date of birth and region, and only if find_appointment returned nothing. Do not call it for a caller who already has appointments — they are already registered. Name and region must be in English even if the caller spoke Hindi — translate what they said, never write Devanagari script or a phonetic transliteration into this tool.

book_appointment

Book an appointment into a slot. Only call this after the caller has explicitly confirmed the summary back to you. Copy ref, slot_ref, date, date_label, weekday, time_label, start_iso, day_bucket, doctor_code, doctor_name and department STRAIGHT from the slot you offered — do not retype, reformat or invent any of them, and never make up a booking reference. When it succeeds, read the ref back slowly as their booking reference, along with the doctor, day and time. patient_name and reason must be in English even if the caller spoke Hindi — translate, never transliterate into Hindi script.

reschedule_appointment

Move an existing appointment to a different time, keeping the same booking reference. The order is always: find_appointment to get the ref, then find_available_slots for the new time, then the caller confirms, then this tool. Copy every field from the NEW slot exactly. Never use this to create a second booking — use book_appointment for that. After it succeeds, read back the new doctor, day and time and confirm the reference has not changed.

cancel_appointment

Cancel a booking. Before calling this you MUST read the doctor, day and time back to the caller and get an explicit yes — cancelling the wrong appointment is a serious error. Ask why they are cancelling and pass it as cancel_reason. After it succeeds, confirm the cancellation and offer to book another time.

Data it manages

Department Doctor Patient Slot Appointment

Permissions it requests

Apps run inside your workspace under least-privilege scopes you can see up front. This app asks for:

Store & manage “department” records in your workspace
objects:department
Store & manage “doctor” records in your workspace
objects:doctor
Store & manage “patient” records in your workspace
objects:patient
Store & manage “slot” records in your workspace
objects:slot
Store & manage “appointment” records in your workspace
objects:appointment
Read your agents' configuration
agents:read
Place phone calls on your behalf
calls:initiate
Dial from the in-app softphone
softphone:dial

Publisher

Hospital Appointments

Marketplace publisher · app reviewed by Telenow before listing.

Your data, your keys

Apps run inside your own workspace under least-privilege scopes. Telenow is the control plane — the AI, telephony and data stay on accounts you own.

Encryption everywhere

Secrets, carrier credentials and API keys stored AES-GCM encrypted; TLS in transit; HMAC-signed webhooks.

Org isolation & RBAC

Strict per-organization data isolation with role-based access for every member.

Full audit log

Every mutating action is logged and filterable, with CSV export for your records.

Telephony compliance

In-app DLT / regulatory flows, TCPA calling windows, and org-wide Do-Not-Call enforcement.

More healthcare apps

Confirm appointments by call and automatically rebook a cancellation from your waitlist before the slot goes cold.

1 installshealthcare
$2.99 free credit on signup

Install Hospital Appointments

Sign up free and get $2.99 in credit — no card required. Connect your number, pick a template, and go live in minutes.