EMR Cowork Substrate
Hospital-wide AI coworkers on the assistant + acknowledgement + collab stack: three data planes, agent identity/delegation, recommender-first.
Read before any “AI coworker”, agent-identity, agent-delegation, or marketing-AI work. Companion docs:
unified-clinical-assistant.md(the skill runtime this builds on),acknowledgement-system.md(the delegation mesh),patient-360.md(the collab surface),navigation-next-best-action.md+ai-training-corpus.md(consent + telemetry + de-id stance),revenue-platform-master-plan/00-MASTER.md(the “agentic substrate, recommender-first” stance this realizes).
Thesis
“Claude Cowork” is AI agents as coworkers — not a chat box, but named, role-scoped agents that sit in the workflow, pick up tasks from a shared queue, do background work, hand a decision back to a human at a gate, and can be handed work by humans. medOS-ultra has already shipped ~70–80% of that substrate under different names (“clinical assistant”, “acknowledgements”, “body-collab”). Building “hospital EMR cowork” here is therefore a reframe + four targeted bridges, applied across the whole hospital (clinical and non-clinical teams), under a three-data-plane safety model.
Verdict: feasible, mostly assembled, gated by safety not by missing tech. The hard, irreversible parts (clinical liability, PDPA/PHI-in-marketing) are handled by the plane model below, not deferred.
1. What already exists (do not rebuild)
Grounded against the codebase 2026-06-05. Status = shipped / partial / design-only.
| Cowork primitive | What it is here | File(s) | Status |
|---|---|---|---|
| Agent runtime (tool loop) | runAgentLoop + SeenMatches hallucination guard |
web/src/services/ai/shared/runner-engine.ts |
shipped (frontend-only) |
| Skill registry | ASSISTANT_REGISTRY — domains w/ toolClass/render/systemPrompt/buildDispatcher/featureFlag/requiredRoles |
web/src/services/ai/registry.ts |
shipped (2 live: clinical-query, cohort-query) |
| Concrete skills | voice-order, smart-diagnosis, symptom-mapper, clinical-query, cohort-query | web/src/services/ai/*/runner.ts |
shipped (voice-order + smart-diagnosis not yet folded into registry) |
| LLM platform (Ollama/RAG) | llm.chat/complete/embeddings.search, models/use-cases/corpora registries |
services/llm/src/api/llm/llmService.ts (+6 mixins) |
shipped |
| LLM/agent audit | llm_audit_log (user_id, use_case, action, tokens, status) |
infrastructure/medbase/migrations/20260514c_llm_platform.sql |
shipped |
| Delegation mesh | AcknowledgementRequest → user/role/department/group + escalation + multi-channel |
web/src/services/ever-foundation/acknowledgement-request/acknowledgementRequest.service.ts; backend services/foundation/.../acknowledgementRequest; read model acknowledgement_requests (mig 035) |
shipped |
| Delegation inbox (FAB) | AcknowledgementInbox mounted in App.tsx, realtime on acknowledgement_requests |
web/src/common/components/medical/builder/acknowledgements/AcknowledgementInbox.tsx |
shipped |
| Task/gig board | user_tasks + auto-assign + PorterGigBoard |
@miniapps/porter-gig-board; infrastructure/.../20260515_auto_assign_system.sql |
shipped |
| Multiplayer surface | body-collab presence/cursors/comments/room-chat (BroadcastChannel ⇄ Supabase realtime) | web/src/services/body-collab/* |
shipped |
| Confirm-gate / permissions | toolClass:'action' → render:'confirm-gate'; policy_gates |
web/src/services/ai/registry.ts; docs/architecture/policy-gates.md |
shipped |
| Background routines | cron_jobs registry + edge functions + orchestrator |
docs/architecture/cron-jobs-registry.md |
shipped |
| Per-tenant flag + role gate | featureFlag + requiredRoles per skill; VITE_TENANT_ID; app-mode |
web/src/services/ai/registry.ts; web/src/config/app-mode.config.ts |
shipped |
| Consent + de-id primitives | ml_export_consent (opt-in default FALSE), ml_access_log, k-anon stance |
docs/architecture/ai-training-corpus.md |
design (pattern reusable) |
Two findings that shrink the build:
- An agent can be the requester on
AcknowledgementRequesttoday with zero schema change —requesteris a plainstring. So “agent proposes → human disposes” (the recommender-first path) rides existing rails. Only delegating to an agent (human→agent, agent→agent) needs a newAckRecipientType: 'agent'. - The skill registry’s
toolClassalready encodes the safety contract per capability (read= no commit,action= confirm-gate,form-fill= open prefilled). Coworkers inherit it.
2. The four gaps to close
| # | Gap | Why it’s needed | Size |
|---|---|---|---|
| G1 | Agent identity (agent-as-actor) | An agent must be a valid assignee, audit author, ack requester/recipient, collab presence. Today agents are anonymous stateless loops. | new table + audit column |
| G2 | Agent ⇄ delegation bridge | Let an agent post a proposal into the existing inbox/task board, and (later) receive work. | small (requester is free; recipientType:'agent' + proposal envelope) |
| G3 | Backend / background runner | runAgentLoop runs only when a human has the tab open. Coworkers must run when nobody’s watching (overnight coding backlog, prior-auth drafts). |
biggest piece — move/duplicate the loop server-side, trigger from cron_jobs/events |
| G4 | The cowork “team” surface | See your coworkers, their queues, hand work over / take it back. | extension of PorterGigBoard + AcknowledgementInbox, not a new app |
Everything else in §1 is reused as-is.
3. Core abstraction — the Coworker
A coworker = a skill + an identity + a data plane + a role binding + a mode. FHIR-aligned: the
coworker is a Provenance.agent where agent.who = Device (the AI software) and
agent.onBehalfOf = Practitioner/role (its human supervisor). This matches the repo’s existing
“mover-as-Practitioner+Device” stance (hospital-movement-architecture.md).
Entity: cowork_agents (canonical in Mongo foundation; Supabase read model)
cowork_agents
id uuid pk
slug text unique -- 'coder-coworker'
display_name text -- "Coder Coworker (AI)"
avatar_url text
skill_id text -- → ASSISTANT_REGISTRY domain id
data_plane text -- 'clinical' | 'operational' | 'growth' (§4)
consent_scope text null -- null | 'marketing'
serves_role text -- role whose queue it feeds, e.g. 'coder'
supervisor_role text -- human owner + kill authority
mode text -- 'recommender' | 'actor' (default 'recommender')
model_use_case text -- → llm_use_cases.code
trigger_kind text -- 'cron' | 'event' | 'manual'
trigger_config jsonb -- cron expr or hospital_events filter
enabled boolean default false -- OFF by default
kill_switch boolean default false
tenant_id uuid null -- multi-tenant (see Open Q4)
created_by, created_at, updated_at
Audit attribution (extend, don’t replace)
Add agent_id text null to llm_audit_log. Every coworker action lands there with its agent
identity + the RunnerStep[] trace. Human actions keep user_id; coworker actions set both
agent_id (who acted) and user_display of the supervising role (on-behalf-of).
Skill contract (tiny extension to AssistantDomain)
registry.ts’s AssistantDomain gains one optional field so a skill can declare the maximum plane
it may run in (defense-in-depth alongside the agent’s data_plane):
// additive — existing fields unchanged
allowedPlane?: 'clinical' | 'operational' | 'growth';
No other registry change. Voice-order/smart-diagnosis fold in as planned (their existing P0).
4. The three data planes (the safety spine)
Same substrate, three data-access contracts. The plane is enforced at the database grant level, not just app logic — a growth-plane agent’s DB role has no grant on clinical tables.
| Plane | Touches | May read | Write contract |
|---|---|---|---|
| Clinical | PHI, dx, orders, results | encounter_journey_cache (structured), clinical read models |
recommender-first, draft-until-signed, every write via confirm-gate/policy_gates, structured-only LLM payloads, full PHI audit |
| Operational | scheduling, beds, queues, RCM/claims, supply, staffing, consented inbound inquiries | department_queues, admission/bed read models, B2B referral data |
lower clinical risk; still audited; pseudonymize where possible; rides existing gates (policy_gates, auto-assign) |
| Growth | acquisition, campaigns, CRM/loyalty, referral, intl-patient marketing | only marketing_audience (consent-gated, de-identified/aggregate) |
hard-walled from PHI; per-read audit (ml_access_log pattern); bilingual outputs; nothing individually targeted without marketing consent |
The marketing wall (PDPA — the sharp edge)
Under Thailand’s PDPA (and BDMS’s own consent posture) you may not use identifiable clinical data for marketing without explicit marketing-purpose consent. Therefore growth-plane coworkers:
- read only a
marketing_audienceview exposing consented contacts + de-identified aggregates (age band, region, service-line interest in aggregate) — never diagnoses, results, or encounter detail; - run under a dedicated Postgres role with zero grants on
encounter_journey_cacheand all clinical tables (the wall is in the DB, not the prompt); - log every read to an
ml_access_log-style audit with a named clinical/data owner per tenant.
This reuses the consent/de-id machinery already designed in ai-training-corpus.md.
5. The delegation bridge (G2)
The proposal envelope
A coworker never mutates the chart. It produces a proposal and asks a human to dispose of it:
cowork_proposals (new, small)
id uuid pk
agent_id → cowork_agents.id
subject_ref text -- encounter / referral / campaign id
draft_payload jsonb -- the proposed codes / itinerary / campaign brief
write_action jsonb -- the existing write path to call on accept
confidence numeric null
runner_trace jsonb -- RunnerStep[] for audit + "why"
status text -- 'pending' | 'accepted' | 'edited' | 'rejected'
decided_by text null
decided_at timestamptz null
The coworker then creates an AcknowledgementRequest:
createAcknowledgementRequest({
subject: { orderType: 'agent_proposal', orderId: proposal.id, display: '…' },
requester: '<cowork_agents.id>', // ← already allowed (string)
recipient: { recipientType: 'role', roleId: 'coder' },
priority: 'routine',
});
One new AckOrderType: 'agent_proposal' so proposals are first-class + filterable. The
AcknowledgementInbox gets one renderer for that type: shows the draft + Accept / Edit / Reject.
Accept/Edit/Reject = the label
On disposition:
- Accept → call
write_action(the existing human write path — coding write, DialogOrder prefill, task dispatch). Same path a human uses; same gates. - Edit → human modifies, then the edited payload goes through the same write path.
- Reject → no write.
The accept/edit/reject delta is captured to llm_audit_log as the training label (per
feedback_clinical_ai_label_in_workflow — “useful?” not “certify?”). Never written to the chart.
6. The backend runner (G3)
Move/duplicate runAgentLoop server-side (extend services/llm, or a new services/ai backend
microservice — Open Q1). Triggered by cron_jobs (sweep) or hospital_events (reactive). It runs
the agent’s skill, produces a cowork_proposals row + an ack, and stops. It has no write grant
on clinical tables; the human’s Accept is what writes.
cron_jobs / hospital_events
│
▼
backend runner ──reads (plane-scoped views)──▶ runAgentLoop(skill, ctx) [SeenMatches guard]
│ │
│ draft + RunnerStep[] + confidence
▼ ▼
cowork_proposals ───────────────▶ AcknowledgementRequest{requester=agent, recipient=role}
│
AcknowledgementInbox (human)
│
Accept / Edit / Reject ──▶ existing write path + label→audit
Invariant: the runner is decoupled from the alert/critical path and never writes a read model directly (no agent projection writes — same rule as the frontend).
7. BDMS-grounded coworker taxonomy
Grounded in Bangkok Hospital / BDMS (49+ hospitals, 60+ clinical departments, 100k+ international patients/yr; C-suite = President·CFO·COO·CMO·Chief Administrative Officer; strong digital marketing, CRM, physician-liaison referral, and international patient services). Sources at end.
| Hospital team | Cowork agent | Rides on (already in repo) | Plane |
|---|---|---|---|
| Coding / Health Info | Coder (draft ICD-10/9 + DRG) | smart-diagnosis + medical-coder + ack inbox | Clinical |
| Nursing | SBAR / alerts / care-plan draft | kardex + CDS + acknowledgement | Clinical |
| Pharmacy | Compounding worksheet + interaction 2nd-opinion | compounding-room + CDS | Clinical |
| Physicians | Ambient scribe + orders + differentials | scribe (design) + voice-order + smart-diagnosis | Clinical |
| Revenue Cycle (CFO) | Claim draft, revenue-at-risk, prior-auth | NHSO engine + revenue platform | Operational |
| Patient Access / Admissions | Admission coordinator | admission-routing + queues + auto-assign | Operational |
| International Patient Services | Navigation, interpreter prep, records-transfer (consented), itinerary | acknowledgement + tasks + FHIR bundle export | Operational + consent |
| Physician Liaison / Referral | Referral analytics, referring-doc outreach (B2B) | tasks + LLM + analytics | Growth (B2B, low PHI) |
| Marketing & Comms | Campaign content/analytics (EN+TH), channel ops | LLM platform + aggregate views | Growth (consent/aggregate) |
| CRM / Loyalty / Patient Experience | Segmentation, NPS analysis, re-engagement | LLM + aggregate views | Growth (consent-gated) |
| HR / Human Capital | Rostering, credential-expiry, onboarding | cron_jobs + tasks | Operational (staff data) |
8. First three vertical slices (one per plane)
Slice A — Coder coworker (Clinical)
- Agent:
coder-coworker, skillcoding-draft(newform-filldomain on smart-diagnosis toolssearchIcd10/searchSnomedCt/mapSnomedToIcd10+ aproposeCodingterminal),data_plane:'clinical',serves_role:'coder',mode:'recommender',trigger_kind:'event'(encounter discharged) or cron sweep of newly-discharged uncoded encounters. - Flow: runner reads the discharged encounter’s structured summary → drafts codes → writes
cowork_proposals+ ack to rolecoder→ coder opens it inAcknowledgementInbox→ Accept/Edit/Reject → on Accept the existing coding write path persists; gesture → label. - Reuses: smart-diagnosis runner/tools, runner-engine, AcknowledgementRequest, AcknowledgementInbox, llm + audit.
- New:
cowork_agents/cowork_proposals,agent_proposalack type + renderer, thecoding-draftskill, backend trigger.
Slice B — International-Patient / Referral coworker (Operational)
- Agent:
ips-coordinator, skillips-itinerary(+referral-analyticsfor the liaison persona),data_plane:'operational',consent_scope:null,serves_role:'international-patient-services'. - Flow: trigger on inbound international inquiry / new referral → runner drafts the patient
journey itinerary (appointments to book, interpreter language, records-transfer checklist via
web/src/utils/fhir-bundle-export.ts, consented) or a referral analytics summary for the physician liaison → proposal as auser_taskassigned to the IPS team (auto-assign) → coordinator reviews/dispatches. - Data plane: patient/referrer initiated contact (consented purpose); no clinical decision-making; records-transfer only with consent; B2B referral data is non-PHI.
- Reuses: user_tasks/PorterGigBoard, acknowledgement, FHIR bundle export, auto-assign.
- New: the IPS skill + trigger.
Slice C — Marketing/Growth coworker (Growth) — + the consent wall
- Prerequisite (build first):
marketing_audienceview — consented contacts (marketing_consent = true) + de-identified aggregates only; no clinical columns. RLS so the growth-plane DB role canSELECTonly this view and has no grant on clinical tables. Per-read audit (ml_access_logpattern). - Agent:
campaign-coworker, skillcampaign-draft(segment summary + bilingual EN/TH content),data_plane:'growth',consent_scope:'marketing',serves_role:'marketing',trigger_kind:'manual'|'cron'. - Flow: runner reads
marketing_audience(aggregate) + LLM → drafts campaign concept / segment summary / content → proposal asuser_taskto the marketing team → marketer reviews/approves. The agent cannot read who has which diagnosis — the wall is in the DB. - Reuses: LLM platform, tasks, audit, consent/de-id pattern from
ai-training-corpus.md. - New:
marketing_audienceview + RLS + growth DB role + marketing-consent source, thecampaign-draftskill. PDPA review of the consent source (Open Q5).
9. Invariants (hard rules)
- No autonomous clinical writes. Coworkers PROPOSE; humans DISPOSE via the existing confirm-gate/
policy_gates. - Plane confinement is enforced at the DB grant level, not just app logic.
- Growth-plane coworkers never read PHI — only
marketing_audience; their DB role has zero clinical grants. - Every coworker action is audited in
llm_audit_logunderagent_idwith theRunnerSteptrace. - Off by default — per-tenant
enabled+kill_switch+ role gating; ship disabled. - Accept/Edit/Reject is the label — captured to the audit log, never written to the chart.
- Recommender → actor promotion is per-tenant, evidence-based, reversible (kill switch); default
mode = recommender. - No hallucinated entities — keep
SeenMatches; terminal proposals may only reference catalog IDs surfaced during the run. - Bilingual outputs (local + English) per repo i18n/seed rule.
- A coworker never writes a read model directly (no agent projection writes) — it rides backend write paths + the orchestrator, same as a human.
10. Phased rollout
| Phase | Deliverable | Demo-ready? |
|---|---|---|
| P0 ✅ | Agent identity: cowork_agents, llm_audit_log.agent_id, AssistantDomain.allowedPlane. No coworker runs yet. Laid down 2026-06-05 — infrastructure/medbase/migrations/20260605a_cowork_agents.sql + web/src/services/ai/registry.ts. |
✅ done |
| P1 ✅ | Delegation bridge: cowork_proposals, AckOrderType:'agent_proposal', Inbox renderer w/ Accept/Edit/Reject → label. Manual trigger. Built 2026-06-05 — mig 20260605b, cowork-proposal-decide edge fn, CoworkProposalPanel(+Connected) + Inbox branch, seed-cowork-proposal-demo.mjs. |
✅ built |
| P2 ✅ | Backend runner: llm.coworkRun mixin in services/llm (modules/coworkRunner/) — reads enabled cowork_agents, drives the skill via llm.complete (Ollama), writes cowork_proposals + agent_proposal ack, audits under agent_id. Built 2026-06-05 (verifies on backend deploy; needs a seeded cowork.default use_case + an enabled agent to run). |
✅ built |
| P3 | Slice A — Coder (clinical). End-to-end clinical vertical. | ✅ |
| P4 | Slice B — IPS/Referral (operational). | ✅ |
| P5 | Growth foundation: marketing_audience consent-gated view + RLS + growth DB role + per-read audit. |
infra |
| P6 | Slice C — Campaign (growth). | ✅ |
| P7 | Cowork “team” surface (coworker roster + queues; recipientType:'agent' for human→agent / agent→agent); promotion governance. |
✅ |
P0→P3 is the shortest path to a real, safe clinical demo. P5→P6 adds the marketing plane behind the consent wall.
11. Open questions
- Backend runner home — ✅ RESOLVED 2026-06-05: host in
services/llmas a separablecowork.*module; extract toservices/coworkat P7. The runner’s first job (P3 Coder) is “LLM + tools + RAG → propose” — exactly whatchatOrchestratoralready does; reuses the Ollama connection,llm_audit_log, andllm_use_cases(a skill ≈ a use-case row + tools), and avoids a greenfield deploy target (the deploy list is finicky). A hard module boundary (cowork.*namespace, depends onllm.*via the broker only — no internal imports) keeps later extraction mechanical when it grows scheduling-ownership / multi-agent / agent-to-agent. Triggers ride the existing edge-fn → NestJS bridge (cf.close-encounter-billing): discharge event → hospital_events → encounter-orchestrator →cowork.run. recipientType:'agent'(human→agent / agent→agent delegation, P7) — confirm Mongo entity +acknowledgement_requestsmigration.- Collab presence for agents — surface a coworker as a body-collab presence on Patient 360 (read-only, @-mentionable)? Phase 7.
- Multi-tenancy —
cowork_agents.tenant_idvs the on-prem single-tenant reality (cloud tenancy is a separate cross-cutting migration permenu-price-ledger.md). - Marketing consent source of truth — ✅ RESOLVED 2026-06-05: dedicated
marketing_consentstable; do NOT reuseml_export_consent. PDPA consent is purpose-bound — ML-training-export consent ≠ marketing consent; reuse = purpose-creep (a violation). The record must be opt-in (default false), per-channel (email/SMS/push), withdrawable (withdrawn_at), and versioned (consent_version+source) — a boolean column is insufficient. P5 buildsmarketing_consents+ themarketing_audienceview (joingranted = true AND withdrawn_at IS NULLfor the purpose/channel). Consent-capture UI + notice text = separate workstream, DPO/legal sign-off required before go-live. (Data shape only; not legal advice.) - Promotion criteria recommender→actor — acceptance-rate threshold, per-skill, per-tenant evidence bar?
Sources (Bangkok Hospital / BDMS grounding)
- BDMS Organization / Executive Management — C-suite functions (President, CFO, COO, CMO, Chief Administrative Officer)
- Bangkok Dusit Medical Services — Wikipedia — network scale, 60+ departments, 100k+ international patients
- BDMS digital marketing portfolio (QUO Global) — brand/marketing-comms function
- Physician Liaison role and referral liaison strategy — referral/liaison coworker grounding
- International patient / medical-tourism navigator role (PMC) — IPS coordinator grounding
Created 2026-06-05. Status: design. Realizes the “agentic substrate, recommender-first, opt-in” stance from revenue-platform-master-plan/00-MASTER.md on top of the shipped assistant + acknowledgement + collab substrate.