Bot Extension Substrate
Universal automation-bot substrate (twin/PRM/marketing/insurance/intelligence) via trigger sources + medos.* SDK + cowork plane.
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-managementGraph tab) and tohospital-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_eventsfan-in: every source normalizes into the same{event_type, scope, payload}shape the executor already consumes, sohandleEventTriggerstays 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.
How it links to the HORUS twin (both directions)
┌──────────────── 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_signalbot wakes whencardiac.occupancy.pctcrosses 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_edgesgraph 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)
- Recommender-first. Outward-facing / cross-plane actions go through
medos.propose→ human Accept/Edit/Reject. No autonomous send or clinical write. - Plane isolation at the DB-grant level. Growth role has zero clinical grant;
marketing/PRM gated on
marketing_consents+ do-not-contact. - Full audit. Every run already logs to
automation_bot_executions+automation_bot_audit; LLM-assisted steps addllm_audit_log(withagent_id). - Twin reads are recommender-only and fail-soft to seed (
isLivebadged). - 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) |