medOS ultra

Bot Extension Substrate

Universal automation-bot substrate (twin/PRM/marketing/insurance/intelligence) via trigger sources + medos.* SDK + cowork plane.

7 min read diagramsUpdated 2026-06-10docs/architecture/bot-extension-substrate.md

Design, not yet built. Status: 2026-06-10. Companion to the shipped automation bot engine (automation_bots + bot-executor + the Bot Org Chart /admin/automation-management Graph tab) and to hospital-decision-twin.md, emr-cowork-substrate.md, enterprise-cowork-catalog.md.

The question

How do we link the automation bot to the HORUS twin, and can we make one extension substrate that’s usable for many things — PRM, marketing, insurance, intelligence decision-making?

Answer: yes, and you don’t build N engines. The bot engine we shipped is already domain-neutral. Look at what it is:

trigger (event│cron│manual│questionnaire)
   → bot-executor (sandboxed Deno) hands the bot a capability SDK (medos.*)
      → actions (notify│emit│chain│api) + full audit (automation_bot_executions)

Nothing in that pipeline is clinical. Only three seams are currently clinical-flavored. Extend those three seams and the same engine drives PRM, marketing, insurance, and intelligence — each as data + capability grants, not a new engine.

Seam Today To generalize
1. Trigger sources (what wakes a bot) hospital_events, cron, manual, questionnaire + twin_signal, crm_event, coverage_event, connector_event, ack_event
2. Capabilities (what a bot can do) medos.read / api / notify / emit / chain / secrets / log + medos.twin, medos.crm, medos.coverage, medos.connector, medos.propose
3. Identity + governance (who it is, what it may touch) per-bot allowed_services / allowed_tables + cowork_agents identity, plane (Clinical/Operational/Growth), allowed_capabilities, consent gate

Everything below is additive. Default config = today’s behavior unchanged.


Seam 1 — Trigger sources (one fan-in, many event streams)

bot-executor/index.ts already switches on trigger_config.type and dispatches by ingress action (event_trigger, cron_dispatch, execute). Adding a trigger source is: (a) one new type value, (b) one new ingress action OR a Postgres trigger on the source table that POSTs to the executor (exactly how notify_bot_executor() works on hospital_events).

New trigger type Fires when… Ingress
twin_signal a twin_metric_* value crosses a configured band, or a twin scenario completes / breaches a guardrail a pg_cron band-evaluator (reuses the cron path) + a trigger on twin_scenario_runs
crm_event lead created, opportunity stage change, campaign sent, ticket opened, patient lapsed / no-show trigger on crm_* tables (the commercial spine, mig 20260605c)
coverage_event eligibility/authorization/scheme changes mid-encounter trigger on encounter_journey_cache.insurance_context (see insurance-aware-workflows.md)
connector_event a registered external system delivered/failed a sync trigger on sync_run / connector_liveness_v (connector catalog)
ack_event a cowork_proposals / acknowledgement_requests row is decided trigger on the ack tables → enables chains that wait for a human decision

The cleanest implementation is a thin domain_events fan-in: every source normalizes into the same {event_type, scope, payload} shape the executor already consumes, so handleEventTrigger stays unchanged and bots match by pattern (matchesAnyPattern) exactly as today.


Seam 2 — Capabilities (the medos.* SDK is the extension point)

The SDK in _shared/bot-sdk.ts is already capability-gated (checkServiceAccess, checkTableAccess, maxApiCalls, dry-run mocking, per-call audit log). Add namespaces the same way — each behind an allowed_capabilities check:

// new SDK namespaces (all gated; all dry-run-aware; all audited)
medos.twin = {
  metric(name, scope?): Promise<{ value, series, isLive }>,   // reads twin_metric_* RPCs (sensor)
  readiness(): Promise<ReadinessRow[]>,                        // data coverage
  runScenario(initiative): Promise<ScenarioResult>,           // DES+MC, read-only
};
medos.crm = {                                                 // Growth plane + consent only
  query(entity, filters), draftTask(...), draftCampaign(...),
};
medos.coverage = { get(encounterId), checkEligibility(...) }; // insurance_context (read)
medos.connector = { call(connectorCode, op, params) };        // push/pull a registered connector
medos.propose = (proposal) => Promise<void>;                  // ← the universal, safety-critical output

medos.propose is the linchpin. Every outward-facing or cross-plane action funnels through it: the bot writes a cowork_proposals row (mig 20260605b) that lands in the Acknowledgement Inbox as Accept / Edit / Reject. So a marketing or insurance bot is exactly as safe as a clinical one — it proposes, never acts. Accept/Edit/Reject is also the training label (the cowork contract).

medos.twin.metric() reuses the existing registry verbatim: each metric = { rpc, valueCol, unit } over a twin_metric_* function with the uniform (p_window_days, p_ward_ids) signature, fail-soft to seed (isLive:false). Adding a metric a bot can watch is one registry row + one SQL function — no engine change.


Seam 3 — Identity, plane & governance (reuse the cowork substrate)

Bind every bot to a cowork_agents identity (mig 20260605a) carrying an allowedPlane. Plane is enforced at the DB-grant level (the 3-data-plane model from emr-cowork-substrate.md) and re-checked in the capability layer:

Plane Who lives here Grants Example bots
Clinical PHI-touching clinical read models sepsis escalator, lab critical
Operational ops/finance, no marketing queues, billing, coverage, twin insurance preauth, capacity sensor
Growth marketing/PRM, zero clinical grant, consent-walled marketing_audience view, marketing_consents recall outreach, campaign proposer

Extend automation_bots with domain, plane, agent_id (FK cowork_agents), allowed_capabilities[]. The plane decides which SDK namespaces resolve and which DB role the executor assumes. A Growth bot physically cannot read a chart.


                    ┌──────────────── HORUS Decision Twin ────────────────┐
                    │  twin_metric_* RPCs · readiness · scenario engine    │
                    └───────▲───────────────────────────────────┬─────────┘
            sensor (read)   │                                   │  actuator (propose)
                            │                                   ▼
   twin_signal trigger ─► [capacity-sensor bot] ─edge─► [triage bot] ─edge─► [domain-action bot]
   (band crossed /          reads medos.twin.metric()        routes by         medos.propose(...)
    scenario breach)        → medos.propose("open 2 beds")   plane/domain      → Ack Inbox (human)
  • Bot → Twin (sensor → actuator): a twin_signal bot wakes when cardiac.occupancy.pct crosses a band (or a scenario’s p10 breaches a guardrail), reads the metric, and proposes an operational move (“open 2 step-down beds”, “shift 1 nurse FTE”) into the inbox — recommender-first.
  • Twin → Bot (twin as orchestration brain): the twin’s existing agents (workforce / supply / reimbursement) emit recommendations; an ack_event / scenario-complete bot fans each out to the right surface (a CRM task for a referral-liaison, an insurance preauth, a notification).
  • The automation_bot_edges graph we just shipped is the wiring diagram for these cross-domain handoffs. The Bot Org Chart is the picture of the mesh.

One substrate, four domains (all = data, not new engines)

Domain Trigger Reads Proposes Plane
PRM crm_event (no-show / lapsed) crm_contacts, last visit recall outreach on a consented channel Growth
Marketing twin_signal (OR utilization low next week) twin metric + marketing_audience targeted campaign Growth
Insurance coverage_event (eligibility lapsed mid-stay) insurance_context re-bind / preauth task for AR Operational
Intelligence twin_signal (forecast breach) twin metrics + readiness grounded recommendation → HORUS decision-protocols, 4-role sign-off Operational

Adding a domain = seed an extension kit (rows: trigger sources + capability grants + plane + a template-bot library + which surface it shows on) — the same “drop seed files, zero TypeScript” idiom as market packs and connector packs.


Invariants (carried from the cowork + twin contracts)

  1. Recommender-first. Outward-facing / cross-plane actions go through medos.propose → human Accept/Edit/Reject. No autonomous send or clinical write.
  2. Plane isolation at the DB-grant level. Growth role has zero clinical grant; marketing/PRM gated on marketing_consents + do-not-contact.
  3. Full audit. Every run already logs to automation_bot_executions + automation_bot_audit; LLM-assisted steps add llm_audit_log (with agent_id).
  4. Twin reads are recommender-only and fail-soft to seed (isLive badged).
  5. Off by default. Per-tenant feature flag + kill switch; default config = today’s clinical behavior, byte-identical.

Rollout

Phase Scope Risk
P0 (demo-ready) medos.twin (read-only sensor) + medos.propose; twin_signal trigger + band-evaluator cron; bind bots to cowork_agents + plane; one end-to-end “Cardiac step-down capacity” sensor bot; plane swimlanes on the Bot Org Chart additive, zero clinical risk
P1 PRM / Marketing kit (crm_event + medos.crm, Growth plane, consent) Growth-plane only
P2 Insurance kit (coverage_event + medos.coverage) Operational
P3 Connector-driven kits; automation_bot_edges drives auto-chaining (executor reads edges after a successful run and fires downstream bots — turning the graph from documentation into runtime) additive

Files this touches (when built)

Layer File
SDK infrastructure/medbase/functions/_shared/bot-sdk.ts (+ medos.twin/crm/coverage/connector/propose)
Executor infrastructure/medbase/functions/bot-executor/index.ts (+ twin_signal / domain_events dispatch)
Schema new mig: automation_bots.{domain,plane,agent_id,allowed_capabilities} + band-config + bot_extension_kits
Triggers PG triggers on crm_*, sync_run, cowork_proposals, twin scenario table → POST executor
Surface web/.../automation/AutomationBotGraph.tsx (plane swimlanes, domain color, cross-domain edges)
Twin reader web/packages/intelligence-kit/src/horus/twin/readers/index.ts (LIVE_METRICS registry — already the model)
Ask Anything