Skip to main content
The 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

Subscription is explicit: without 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 ("*").

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

Suppress your inactivity timer between report.generation_started and report.settled. Generation runs for tens of seconds with no input from the clinician —it looks exactly like idleness— and closing the widget there loses the note. The two events always come in pairs: the SDK guarantees the settled from every code exit, not from event names.

lifecycle

Keeping an external session alive

If your system logs users out on inactivity, what it needs is periodic activity pings, not the started/stopped edges. That is what recording.heartbeat is for.
Your integration holds no state. And it fails closed: if audio stops flowing for any reason —microphone unplugged, network down, zombie connection, frozen tab— the beats stop and your own timeout runs.
Why not build it on recording.started / recording.stopped? Because twenty minutes can pass between the two with no other event, so you would need your own setInterval between them. And if you ever miss the stopped (a reload mid-recording, a hung tab, a throw in your handler), that interval keeps the session alive forever — precisely the failure an inactivity logout exists to prevent.
The heartbeat comes from the very spot the SDK uses internally to know a recording is healthy: the send of each audio chunk, which only happens with the transcriber ready, the connection open and the microphone capturing. It keeps beating while the clinician is silent, because audio chunks are sent regardless.

Full recipe for an inactivity timer

With 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 setState safely, and a slow handler does not delay recording.
  • Ordered. seq is 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 patientid or userid remounts 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 second onEvent were 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.name or event.family and 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 unconstrained string 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