next-ehr-minimal example: your application supplies the encounter context, controls the embedded experience, and decides how reviewed output updates its own form.
The video walks through the completed EHR workflow, from recording an encounter to reviewing documentation and syncing it into the clinical form. The guide below covers the setup and implementation: how to run the example, configure Corti Assistant, and adapt the integration to your own application. For the component’s methods and events, see the Web Component API.
What you’ll build
The fictional EHR contains patients, appointments, encounter types and a clinical documentation form. You add Corti Assistant to its Annual checkup workflow without replacing the surrounding application. The clinician opens a consultation with patient context already visible, records the conversation, generates a SOAP document, and reviews it inside Corti Assistant. Only after the clinician chooses to sync do the generated sections populate the EHR’s Subjective, Objective, Assessment and Plan fields. The host then collapses Corti Assistant, with a Show assistant button to reopen it. This is a small working integration, not a production EHR. Use synthetic patient data and a test conversation throughout.Run the example
Prerequisites
- Git, Node.js 22 LTS and npm. The example uses Next.js 16 and a native SQLite dependency.
- A browser with microphone access and network access to your Corti environment.
- A Corti project with Embedded Assistant access, a user-based OAuth client and a demo Corti user. The steps below explain how to obtain these.
- Familiarity with React and TypeScript to adapt the implementation.
1
Clone and install
Clone the examples repository, install dependencies in
embedded-assistant/react/next-ehr-minimal, and create the local environment file:Terminal
2
Set up Corti
Sign in to Corti Console and select the project you will use for the demo. If you need a project, create one in the intended region. The client and user must belong to the same project and tenant/environment combination.Under API clients, choose Create API client. Give it a descriptive name, choose Embedded Assistant under How will this client be used?, then select ROPC (Resource Owner Password) as the Authentication method. Create the client and open its details. For an existing client, verify that it uses this authentication method.Copy the full Client ID into
CORTI_CLIENT_ID, Environment ID into CORTI_ENVIRONMENT, and Tenant name into CORTI_TENANT_NAME. Use the created client’s full ID, not just the name or suffix entered in the creation form. The usual public environments are eu and us, and the usual tenant is base; copy your actual values instead of assuming these defaults. See Environments & Tenants.For the demo user, open Customers, select the relevant customer, and choose Add user. If necessary, create a test customer first. Enter a synthetic name and an email address you control. Enable Manual Invite and enter Set Password to establish credentials for this local demo. Follow the password requirements shown in the form. Put that user’s email in CORTI_USER_EMAIL and the password you set in CORTI_USER_PASSWORD. Alternatively, use an existing enabled Corti user whose credentials you know.User creation requires project developer access or the appropriate administrator access. If you cannot access customer/user management or cannot provision the required user-based client, ask your project administrator or contact Corti to provision them for your project. Request the environment, tenant name, ROPC client ID and a demo user with a known password. Do not use your Corti Console login password or try to extract the Console’s internal Studio credentials.ROPC exchanges a user’s email and password for tokens. It therefore needs both a client permitting the password grant (also called Direct Access Grants) and an enabled user in that tenant. Selecting ROPC in Console configures the client for this grant; this example does not use a client secret or a machine-to-machine client.3
Configure environment variables
Edit the
.env created above. All six values below are read on the server; none needs a NEXT_PUBLIC_ prefix. The supplied .env.example starts with staging-eu and base: replace these with the environment and tenant of your client, rather than mixing staging and production credentials.There is no separate Assistant URL variable:
GET /api/config constructs https://assistant.${environment}.corti.app. GET /api/auth calls getCortiRopcToken() from lib/corti-server-auth.ts. That helper creates a CortiAuth instance and calls auth.getRopcFlowToken(). The route returns the token fields expected by api.auth(), with mode: "stateful".4
Start the EHR
Run
npm run dev in the example directory and open http://localhost:3000. Next.js serves both the EHR and its /api routes; there is no separate Express server. If port 3000 is occupied, use the local URL printed by Next.js.The application creates and seeds its SQLite database on first use. Open Patients, select a patient, and choose New consultation, then Annual checkup. An annual-checkup appointment’s new consultation screen uses the same integration.Confirm that the EHR form loads and the embedded panel reaches Corti assistant ready. Allow microphone access, record a synthetic encounter, generate and review a document, then use the document’s sync action. The SOAP fields should populate and the panel should collapse. Save consultation is a separate EHR action that persists the form.CORTI_* values, restart the development server after changing .env, and confirm the client permits ROPC and the demo user is enabled. A database startup error usually means EHR_SQLITE_PATH is missing or not writable. If the fixed template cannot be resolved, confirm your project can use the standard SOAP template referenced by the example; contact Corti if access needs enabling. Do not substitute a different template without updating the section mapping too.
Integration model
The EHR is the host and the driver of the workflow. Communication crosses the Web Component API in both directions:
React uses a wrapper around the same Web Component that JavaScript, TypeScript, Vue, Angular and web-based wrappers such as Electron can render directly. Native .NET, Swift or Kotlin hosts typically put a small HTML host page and the component in a WebView. This example follows the direct React/web path.
End-to-end flow
The complete round trip is:1
Mount and initialise
Fetch
/api/config and /api/auth, mount CortiEmbeddedReact hidden, and wait for onReady. Call api.auth(), api.configureApp() and api.setInteractionOptions().2
Create and open the interaction
Call
api.createInteraction(), then api.addFacts() with patient context. Await api.navigate({ path: `/session/${interaction.id}` }), then call corti.show().3
Record, generate and review
The clinician records in Corti Assistant, reviews the extracted facts as needed, generates the document, and reviews or edits its content. Generation alone does not update the EHR form.
4
Sync into the EHR
The clinician explicitly syncs. The host handles
document.synced, reads event.detail.payload.document.sections, maps known section keys to its form fields, and updates local form state. It calls corti.hide() and collapses the panel. The clinician can reopen Corti Assistant or save the consultation in the EHR.Keep the first workflow small
A predictable output contract makes the first round trip easier to verify. Although the EHR supports several consultation types, this example enables Corti Assistant only forannual-checkup and uses:
- One fixed standard SOAP template with known section identifiers.
- A forced template for the first document, with default-template selection controls disabled.
- At most one generated document per interaction.
- English-only spoken-language options and document output languages.
- An explicit sync action and four known destination fields.
- One configured demo user for every local request.
Project structure
These are the files that matter to the integration; all paths are relative to the example directory.Enable Corti Assistant for annual checkups
Start with the EHR’s existing route. In the patient consultation page, one condition adds the integration above the normal form:app/patients/[id]/new-interaction/page.tsx
appointment.consultationType. Other workflows keep their existing form without the embed. Corti Assistant does not decide which EHR encounter types should expose it.
Mount Corti Assistant
CortiAssistantEmbed renders the React wrapper from@corti/embedded-web/react:
components/corti-assistant-embed.tsx
useCortiEmbeddedApi(cortiRef) for Embedded API calls, and by show()/hide() for visibility. baseURL comes from the server configuration. onReady starts initialisation, onError updates the host’s error state, and onEvent receives events such as document sync. The component fills the host container, which starts at 500 pixels high; embedKey lets the retry action remount it.
Mounting provides a place in the UI. It does not authenticate the user or create the clinical interaction.
Initialise the interaction
The panel’shandleReady() guards against repeated initialisation and calls startCortiAssistantSession(). This excerpt is the helper’s call sequence:
lib/corti-assistant-ehr-integration.ts
configureApp() hides the interaction title, while setInteractionOptions() constrains the clinical workflow. The helper then creates the interaction, adds context, navigates to it and shows the component. Its name uses “session”, but the created Corti resource is an interaction.
The example already includes a readiness timeout, status messages and a retry button. For production timeout and navigation-completion handling, follow Embedded Reliability, Timeouts, and Recovery.
Configure the workflow
buildSoapInteractionOptions() in the same helper returns the options passed before interaction creation:
lib/corti-assistant-ehr-integration.ts
templateId comes from CORTI_SOAP_TEMPLATE_ID in lib/corti-soap-template.ts. It is a concrete standard SOAP template identifier, not a value to create in Console for this walkthrough.
force-first-document selects that template for the first document, allowUserSelection: false disables default-template selection controls, and maxGenerated: 1 keeps the workflow to one generated document. sync: true exposes the deliberate hand-off to the EHR. Both language restrictions use en.
See Configuration Scenarios for examples, configureApp() and setInteractionOptions() for the current configuration methods, and Embedded API Supported Values for lookup values. This example references a fixed template; it does not construct an inline template.
Add patient and encounter context
Avoid asking the clinician to re-enter information the host already knows. AnnualCheckupCortiAssistant derives an identifier from the appointment ID, or from the patient ID plusannual-checkup. The panel appends a timestamp when creating the interaction:
components/corti-assistant-panel-client.tsx
annual-checkup is the host’s workflow key; the actual Corti encounter payload uses type: "first_consultation". Do not substitute one for the other. See createInteraction().
buildCortiAssistantVisitConfig() builds facts for the patient’s name, date of birth, age, gender, chronic conditions, allergies and visit reason. Each fact has group: "other"; the helper normalises whitespace and truncates each text to 100 characters. These are example-specific choices, so review truncation before adapting this to longer clinical context.
api.addFacts(visitConfig.patientFacts) runs after interaction creation. Use addFacts() for its input contract. The form’s clinician label does not select a Corti user: authentication still uses the one demo identity from .env.
Generate and review documentation
Once initialisation completes, the clinician records, generates and reviews within the embedded UI. The host does not build its own microphone lifecycle, realtime transport or document editor for this flow. Keep the EHR form alongside that experience. At this stage, the generated document belongs to Corti Assistant; the host has not yet applied it to the form.Handle document sync
Generation and application are separate decisions. In the panel’s event handler, only the explicit sync event triggers form updates:components/corti-assistant-panel-client.tsx
event.detail.name and the payload as event.detail.payload. The document.synced reference distinguishes public metadata from the confidential payload containing the full document. The mapper consumes the latter: payload.document.sections, with each section’s key and text. The public metadata alone cannot populate the form.
Treat this payload as clinical data, not telemetry. Do not log document bodies, patient facts, passwords or tokens in application logs.
Populate the EHR form
The fixed SOAP template makes a direct mapping possible. lib/corti-soap-template.ts records the real template and section identifiers used by this example:lib/corti-soap-template.ts
section.key against them avoids relying on human-readable headings such as Actions and Plan. If you change the template, verify its returned section keys and update the mapping together.
lib/corti-assistant-sync.ts translates sections into a partial update, then sends it to the form store:
lib/corti-assistant-sync.ts
lib/corti-assistant-sync.ts
Not recorded; missing sections leave existing fields unchanged. Known non-empty sections replace their destination values, so syncing again can overwrite manual edits in those fields. Other fields, such as vitals and the clinician, are not populated by this mapping.
updateConsultationFormFields() calls the Zustand store’s updateFields(), which merges the partial update into existing values. ConsultationForm subscribes to store.fields and binds its inputs through fieldProps(), so the four SOAP fields immediately display the synced text.
Before sync, only Corti Assistant holds the generated document. After sync, the fields populate and the host hides and collapses the embed. This is the transition illustrated in the introductory walkthrough. It is still a form update, not an EHR save: the clinician reviews the populated fields and chooses Save consultation to persist them through the host’s server action.
What you have now
You have an EHR-owned workflow that embeds Corti Assistant, supplies patient and encounter context, constrains documentation, and lets the clinician record and review without leaving the screen. An explicit sync returns structured sections, and the host maps known SOAP sections into its own editable form. The integration is useful, but intentionally not production-complete.Production considerations
- Multi-user identity and tokens: Establish which Corti user the signed-in clinician should act as. Do not reuse one
.envidentity for every clinician. Protect the host token route and design renewal, expiry and logout handling using Authentication for Embedded Users and auth(). Multi-user or brokered authentication is a separate integration step, not implemented here. - Loading and recovery: Build on the demo’s loading, error and retry states. Follow the reliability guide for timeouts, interrupted communication and recovery. Persist interaction IDs where appropriate so retries and returning to an encounter do not unintentionally create new interactions.
- Permissions and security: Enforce clinician/patient access in your backend, obtain microphone permissions and use HTTPS outside localhost. Apply appropriate data residency, retention and secret-management controls; a local SQLite demo is not a production storage design.
- Sync and persistence: Validate incoming payloads, preserve the active patient/encounter association, define overwrite/conflict behaviour, and distinguish a populated form from a successfully saved record. Handle missing sections, failed saves and navigation away from unsaved work.
- Telemetry and audit: Record operational outcomes and audit who accepted and saved documentation without putting clinical payloads or credentials in ordinary logs. Plan workflow-specific failure states and support diagnostics.
Where fixed mapping stops scaling
Natural next steps include flexible document/section mapping, personal templates, or choosing a structure from encounter type, patient context and workflow requirements. A host-built dynamic inline template is one possible direction; consult the current Configuration Scenarios before implementing it. This guide does not define a new payload or solve arbitrary-template mapping.
Extend the proven round trip incrementally, alongside multi-user authentication and production persistence, rather than treating fixed SOAP mapping as the architecture for every EHR.