Skip to content

Add visit-confirmed to extensions - #425

Open
YPCrumble wants to merge 3 commits into
Medical-Software-Foundation:mainfrom
YPCrumble:add-visit-confirmed
Open

Add visit-confirmed to extensions#425
YPCrumble wants to merge 3 commits into
Medical-Software-Foundation:mainfrom
YPCrumble:add-visit-confirmed

Conversation

@YPCrumble

Copy link
Copy Markdown

What it does

VisitConfirmed is an AI scheduling agent that confirms patient appointments over two-way SMS and voice. This plugin forwards Canvas appointment lifecycle events (created, rescheduled, cancelled, no-show) to VisitConfirmed, which then contacts the patient in the practice's name, answers what they ask, and passes only the exceptions back to staff.

One handler, visit_confirmed.handlers.appointment_events:AppointmentEvents, responding to APPOINTMENT_CREATED, APPOINTMENT_CANCELED, and APPOINTMENT_NO_SHOWED. A create linked via appointment_rescheduled_from is reported as a reschedule.

On PHI

The plugin's payload carries resource IDs and scheduling metadata only, no names or contact details, and it logs none either. To be straightforward about the whole picture rather than just the plugin: VisitConfirmed does read patient demographics, over a separate scoped FHIR connection the practice authorizes under a BAA. The README's Privacy section states both channels explicitly rather than leaving the impression that no patient data leaves Canvas.

It is inbound only and never writes appointments back into Canvas.

Configuration

Two sensitive variables, VISIT_CONFIRMED_API_URL and VISIT_CONFIRMED_API_KEY. The handler fails closed if either is unset: it logs an error and makes no outbound call.

Verification

canvas validate-manifest visit_confirmed   valid
canvas validate visit_confirmed            handler loads cleanly in the sandbox
uv run pytest                              6 passed
uv run pytest --cov                        100% branch coverage
uv run mypy visit_confirmed                clean

mypy.ini is copied from extensions/missed-appointment-notifications so local results match review. Structure follows that plugin as well, including docs/ inside the package.

The README screenshot is generated from docs/canvas-appointment-confirmation.source.html, committed beside it so it can be re-rendered rather than being an opaque binary. It is an illustration of the conversation, not a capture of real patient data.

Source repo

https://github.com/VisitConfirmed/canvas-medical-plugin (public, MIT)

One question for reviewers

Happy to move this if a vendor-branded connector belongs somewhere other than the reference-plugin repo. extensions/nabla suggested here is right, but say the word and I will relocate it.

🤖 Generated with Claude Code

YPCrumble and others added 2 commits July 29, 2026 16:55
Visit Confirmed is an AI scheduling agent that confirms appointments and
recovers no-shows over two-way SMS and voice. This plugin forwards Canvas
appointment lifecycle events (created, rescheduled, cancelled, no-show) to
Visit Confirmed, which then contacts the patient in the practice's name.

The connector sends only resource IDs and scheduling metadata. It transmits no
patient names, phone numbers, emails, or clinical data, and logs none either.
Visit Confirmed resolves contactable details through the practice's own Canvas
FHIR connection under its BAA.

Verified before submission: mypy clean under the same mypy.ini used by
extensions/missed-appointment-notifications, 6 tests passing against
canvas[test-utils] with 100 percent branch coverage, and canvas validate
loading the handler cleanly in the sandbox.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t_time guard

Self-review against this repo's CLAUDE.md plugin checklist found two issues in
the original submission.

The Appointment query did not filter entered_in_error, so a retracted
appointment could have triggered patient outreach. Now filtered, per the
clinical-data rule in CLAUDE.md.

The payload reads patient.id and provider.id, so the query now uses
select_related('patient', 'provider') instead of paying a query apiece.

Adds a defensive guard for a missing start_time, since arrow.get(None) raises.
start_time is declared non-null on the SDK model, so this is precautionary,
matching the guard in extensions/missed-appointment-notifications.

Tests updated to patch the select_related().get() chain, with new cases for the
retraction filter and the missing start time. 8 tests, 100 percent branch
coverage, mypy clean, canvas validate clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@YPCrumble

Copy link
Copy Markdown
Author

Self-reviewed against this repo's CLAUDE.md plugin checklist after opening, and pushed two fixes.

Retracted appointments were not excluded. The query was a bare Appointment.objects.get(id=...), and Appointment carries a nullable entered_in_error FK. Without the filter, an appointment a clinic had retracted could still have triggered patient outreach, which is the worst failure this plugin is capable of. Now filtered.

Related objects cost a query apiece. The payload reads patient.id and provider.id, so the query now uses select_related("patient", "provider").

Also added a defensive guard for a missing start_time, since arrow.get(None) raises TypeError. start_time is declared non-null on the SDK model, so this is precautionary rather than an observed failure, matching the guard in extensions/missed-appointment-notifications.

Tests now patch the select_related().get() chain, with new cases covering the retraction filter and the missing start time. Still 100% branch coverage, mypy clean, canvas validate clean.

One note in case it looks off against the checklist: the manifest uses variables with sensitive: true rather than the secrets array the CLAUDE.md checklist mentions. canvas_cli/utils/validators/manifest_schema.py marks secrets deprecated in favor of variables, and extensions/missed-appointment-notifications uses variables, so I followed the schema. Happy to switch if you'd rather the checklist win.

VISIT_CONFIRMED_API_URL feeds an Http.post carrying the key in an Authorization
header, and nothing validated the scheme. canvas_sdk's Http does not either:
with no base_url its join_url containment check is joined.startswith(''), which
is always true, so an http:// value would send the key in cleartext.

Implemented as a lowercased prefix test rather than urllib.parse.urlparse,
which the plugin sandbox rejects as a disallowed import. Secrets are stripped on
read, matching extensions/missed-appointment-notifications.

Two new tests cover an http:// endpoint and a scheme-less one. 10 tests, 100
percent branch coverage, mypy clean, canvas validate clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant