handleReport, onReportApply and handleExtras callbacks fire at the end of a workflow. They tell you nothing about what happens in between: whether the clinician is recording, whether they are typing, whether SofIA is generating the note. onEvent fills that gap with a stream of typed, PHI-free events.
The use case that motivated it: a HIS that logs users out on inactivity cannot tell “the clinician has been recording a consultation for twenty minutes without touching the keyboard” from “the clinician walked away twenty minutes ago”. With the microphone active and no other activity, it was logging the clinician out mid-consultation.
Events never carry clinical text, report content, what was typed, or patient or clinician identifiers. Every payload field is an enum, a boolean or a number. If you need the clinical content, you already have dedicated channels for it (
handleReport, onReportApply, handleExtras).The two props
eventSubscriptions (or with an empty array) no event is delivered, and the SDK logs a console warning if you pass onEvent without it. It accepts exact names ("recording.started"), family wildcards ("recording.*") or everything ("*").
- React
- Web Component
Shape of an event
sessionId is the same random identifier the SDK uses for its internal analytics. It changes with every patient, so you can correlate a burst of events to one consultation and discard any that arrive after a patient switch.
Event catalogue
recording
activity
Both are throttled to one event every 5 seconds per kind/surface, on the leading edge: the first click or keystroke is emitted immediately and the following ones are suppressed for the window. For idle detection, over-emitting is harmless (your timer just resets); under-emitting would close the app on someone mid-sentence.
report
lifecycle
Keeping an external session alive
If your system logs users out on inactivity, what it needs is periodic activity pings, not thestarted/stopped edges. That is what recording.heartbeat is for.
Full recipe for an inactivity timer
eventSubscriptions={['recording.*', 'activity.*', 'report.*']}. A recording.audio_lost or recording.microphone_disconnected also resets the timer here —reasonable: the clinician is still in front of the screen looking at the warning— but if you would rather treat them as the end of activity, compare on event.name.
Delivery guarantees
- Asynchronous. Every event is delivered on a later microtask, never inside React’s render or on the audio path. Your handler can
setStatesafely, and a slow handler does not delay recording. - Ordered.
seqis monotonic and events arrive in that order. - Isolated. If your handler throws, the SDK catches it and keeps working; it logs a single console warning per page load.
- Stable across patients. Changing
patientidoruseridremounts the widget internally, but your subscription and handler survive without being reassigned. Passing an inline function on every render does not re-subscribe either. - One widget per page. The SDK supports a single
<Omniscribe>/<sofia-sdk>instance. If a secondonEventwere registered, the newest wins and a console warning is logged. - Open set of names. New events may be added in minor releases. Compare on
event.nameorevent.familyand treat unknown names as “something happened”; do not assume a closed set.
For an exhaustive
switch in TypeScript, narrow event.name to KnownSdkEventName. The SdkEvent, SdkEventName, KnownSdkEventName, SdkEventFamily, SdkEventHandler, SdkEventPayloadMap and SdkEventSubscription types are exported from @omniloy/sofia-sdk.Privacy
The design rests on an invariant the SDK’s own compiler enforces: no field of any payload is a free-form string. Errors travel as codes, reports as booleans, typing as the name of the surface. The SDK has a test that reads the type definition and fails the build if an unconstrainedstring appears.
That is why onEvent is not a dump of the SDK’s internal log: log messages are hand-written prose that interpolates identifiers and whole error objects, and that cannot be made safe with a filter. If you need to debug, use the debug prop and the console; if you need clinical content, use the dedicated callbacks.
Next steps
Optional properties
Every prop and callback of the component
Insertion preview
Where
activity.typing with surface: 'note' comes from