Architecture
Domino is a team of agents that use FalkorDB as their only source of truth. They decide which graph queries to run, act by writing findings, tasks and notes back into the graph, remember what the care team decided and did, learn how to route their work, and show the path behind every answer. Each clinic is its own graph: Riverside Family Health and Lakeview Geriatric Clinic in this demo.
Walks the graph to find cascades, loops and combined harm, then records findings.
Checks the evidence, other reasons, safer options and the what-if for each root.
Turns the findings into review tasks and draft notes for humans to approve.
Track 1 · Agents that act on connected data
Track 2 · Agent memory and coordination
Track 3 · A care-team brain
The core query
MATCH (p:Patient {id: $pid})-[:TAKES]->(m:Drug)
WITH p, collect(m.id) AS meds
MATCH (p)-[:TAKES]->(a:Drug)-[:CAUSES]->(:Effect)-[:TREATED_BY]->(:Drug)<-[:TAKES]-(p)
WITH DISTINCT a, meds
MATCH path = (a)-[:CAUSES|TREATED_BY*2..6]->(z:Drug)
WHERE z <> a AND all(n IN nodes(path) WHERE NOT n:Drug OR n.id IN meds)
RETURN [r IN relationships(path) | [startNode(r).id, type(r), endNode(r).id]] AS hopsMATCH (p:Patient {id: $pid})-[ta:TAKES]->(a:Drug)-[c:CAUSES]->(e:Effect)-[tr:TREATED_BY]->(b:Drug)<-[tb:TAKES]-(p)
WHERE a <> b
OPTIONAL MATCH (a)-[:IN_CLASS]->(:DrugClass)<-[:INDEX]-(pt:Pattern)-[:VIA]->(e),
(pt)-[:MARKER]->(:DrugClass)<-[:IN_CLASS]-(b)
RETURN a.id AS from, a.name AS fromName, b.id AS to, b.name AS toName, e.id AS effect, e.name AS effectName,
e.plain AS effectPlain, c.freq AS freq, c.weight AS weight, c.onsetMin AS onsetMin, c.onsetMax AS onsetMax,
tr.line AS line, ta.start AS fromStart, tb.start AS toStart, tb.indication AS toIndication,
collect(DISTINCT pt.id) AS patternsGraph model · knowledge and patients
Graph model · memory, provenance and the care team
Every query Domino runs
There is no other Cypher in the app. User input only reaches FalkorDB as a parameter; reads run as GRAPH.RO_QUERY, so the database itself refuses writes; and the agents can only write through the fixed templates marked write.
MATCH (p:Patient {id: $pid})
OPTIONAL MATCH (p)-[h:HAS]->(c:Effect)
WITH p, collect({id: c.id, name: c.name, plain: c.plain, since: h.since}) AS conditions
OPTIONAL MATCH (p)-[t:TAKES]->(d:Drug)-[:IN_CLASS]->(k:DrugClass)
RETURN p.id AS id, p.name AS name, p.age AS age, p.sex AS sex, p.persona AS persona,
p.headline AS headline, p.story AS story, p.sandbox AS sandbox, p.expiresAt AS expiresAt, conditions,
collect({id: d.id, name: d.name, cls: k.id, clsName: k.name, acb: d.acb, beers: d.beers,
start: t.start, dose: t.dose, prescriber: t.prescriber, indication: t.indication, assumedDate: t.assumedDate}) AS medsMATCH (p:Patient)
WHERE p.sandbox IS NULL
OPTIONAL MATCH (p)-[:TAKES]->(d:Drug)
WITH p, count(d) AS meds
RETURN p.id AS id, p.name AS name, p.age AS age, p.sex AS sex, p.persona AS persona,
p.headline AS headline, meds, p.likely AS likely, p.possible AS possible,
p.acb AS acb, p.topScore AS topScore, p.topLink AS topLink, p.topLinkScore AS topLinkScore, p.avoidable AS avoidable, p.reviewedAt AS reviewedAt
ORDER BY p.persona DESC, p.topScore DESC, p.nameMATCH (p:Patient {id: $pid})-[ta:TAKES]->(a:Drug)-[c:CAUSES]->(e:Effect)-[tr:TREATED_BY]->(b:Drug)<-[tb:TAKES]-(p)
WHERE a <> b
OPTIONAL MATCH (a)-[:IN_CLASS]->(:DrugClass)<-[:INDEX]-(pt:Pattern)-[:VIA]->(e),
(pt)-[:MARKER]->(:DrugClass)<-[:IN_CLASS]-(b)
RETURN a.id AS from, a.name AS fromName, b.id AS to, b.name AS toName, e.id AS effect, e.name AS effectName,
e.plain AS effectPlain, c.freq AS freq, c.weight AS weight, c.onsetMin AS onsetMin, c.onsetMax AS onsetMax,
tr.line AS line, ta.start AS fromStart, tb.start AS toStart, tb.indication AS toIndication,
collect(DISTINCT pt.id) AS patternsMATCH (p:Patient {id: $pid})-[:TAKES]->(m:Drug)
WITH p, collect(m.id) AS meds
MATCH (p)-[:TAKES]->(a:Drug)-[:CAUSES]->(:Effect)-[:TREATED_BY]->(:Drug)<-[:TAKES]-(p)
WITH DISTINCT a, meds
MATCH path = (a)-[:CAUSES|TREATED_BY*2..6]->(z:Drug)
WHERE z <> a AND all(n IN nodes(path) WHERE NOT n:Drug OR n.id IN meds)
RETURN [r IN relationships(path) | [startNode(r).id, type(r), endNode(r).id]] AS hopsMATCH (p:Patient {id: $pid})-[:TAKES]->(m:Drug)
WITH p, collect(m.id) AS meds
MATCH (p)-[:TAKES]->(a:Drug)
MATCH path = (a)-[:CAUSES|TREATED_BY*4..6]->(a)
WHERE all(n IN nodes(path) WHERE NOT n:Drug OR n.id IN meds)
RETURN [r IN relationships(path) | [startNode(r).id, type(r), endNode(r).id]] AS hopsMATCH (p:Patient {id: $pid})-[:TAKES]->(b:Drug)<-[tr:TREATED_BY]-(c:Effect)<-[h:HAS]-(p)
RETURN b.id AS drug, collect({id: c.id, name: c.name, since: h.since, line: tr.line}) AS reasonsMATCH (p:Patient {id: $pid})-[:TAKES]->(d:Drug)-[c:CAUSES]->(e:Effect)
WITH e, collect(DISTINCT {id: d.id, name: d.name, freq: c.freq}) AS drugs
WHERE size(drugs) >= 2
RETURN e.id AS id, e.name AS name, e.plain AS plain, e.kind AS kind, drugs
ORDER BY size(drugs) DESCMATCH (p:Patient {id: $pid})-[:TAKES]->(d:Drug)-[:CAUSES]->(e:Effect)-[:RAISES*0..2]->(h:Effect)
WHERE h.id IN $harms
RETURN h.id AS harm, h.name AS harmName, d.id AS drug, d.name AS drugName, collect(DISTINCT e.name) AS viaMATCH (p:Patient {id: $pid})-[:TAKES]->(d:Drug)-[:IN_CLASS]->(:DrugClass)<-[g:GROUP]-(combo:Combo)
WITH combo, g.g AS grp, collect(DISTINCT d.name) AS drugs
WITH combo, collect({grp: grp, drugs: drugs}) AS groups
WHERE size(groups) = combo.groups
MATCH (combo)-[:HARM]->(h:Effect)
RETURN combo.id AS id, combo.title AS title, combo.why AS why, combo.refs AS refs, h.name AS harm, groupsMATCH (p:Patient {id: $pid})-[:TAKES]->(d:Drug)
WHERE d.acb > 0 OR d.beers IS NOT NULL
RETURN d.id AS id, d.name AS name, d.acb AS acb, d.beers AS beersMATCH (p:Patient {id: $pid})-[ta:TAKES]->(a:Drug {id: $drug})
MATCH (a)<-[:TREATED_BY]-(ind:Effect)
WHERE (p)-[:HAS]->(ind) OR ta.indication = ind.id
MATCH (ind)-[tr:TREATED_BY]->(alt:Drug)-[:IN_CLASS]->(ac:DrugClass)
WHERE alt <> a AND NOT (p)-[:TAKES]->(alt) AND NOT (alt)-[:CAUSES]->(:Effect {id: $effect})
OPTIONAL MATCH (p)-[:TAKES]->(cur:Drug)-[:IN_CLASS]->(:DrugClass)-[av:AVOID_WITH]-(ac)
WHERE cur <> a
OPTIONAL MATCH (alt)-[:CAUSES]->(worse:Effect)<-[:HAS]-(p)
OPTIONAL MATCH (alt)-[:CAUSES]->(pile:Effect)<-[:CAUSES]-(other:Drug)<-[:TAKES]-(p)
WHERE other <> a
RETURN alt.id AS id, alt.name AS name, ac.name AS clsName, tr.line AS line, ind.name AS forCondition,
alt.acb AS acb, alt.beers AS beers, collect(DISTINCT av.why) AS conflicts,
collect(DISTINCT worse.name) AS worsens, collect(DISTINCT pile.name) AS addsMATCH (p:Patient {id: $pid})-[ta:TAKES]->(a:Drug)-[c:CAUSES]->(e:Effect)-[tr:TREATED_BY]->(n:Drug {id: $drug})
WHERE a <> n AND ta.start < $asOf AND NOT a.id IN $exclude
OPTIONAL MATCH (a)-[:IN_CLASS]->(:DrugClass)<-[:INDEX]-(pt:Pattern)-[:VIA]->(e),
(pt)-[:MARKER]->(:DrugClass)<-[:IN_CLASS]-(n)
OPTIONAL MATCH (p)-[h:HAS]->(e) WHERE h.since <= $asOf
RETURN a.id AS from, a.name AS fromName, e.id AS effect, e.name AS effectName, e.plain AS effectPlain, c.freq AS freq,
c.weight AS weight, c.onsetMin AS onsetMin, c.onsetMax AS onsetMax, tr.line AS line,
ta.start AS fromStart, h IS NOT NULL AS diagnosed, collect(DISTINCT pt.id) AS patternsThe same tools run as a stdio MCP server for one clinic (DOMINO_CLINIC): patient tools, the prescriber check, and the care-team brain (search_team_memory, who_knows, care_team_flows, learned_procedures), plus the query catalog and the graph model as MCP resources. Read-only by default; DOMINO_MCP_ALLOW_WRITES=true adds run_review.
{
"mcpServers": {
"domino": {
"command": "npx",
"args": ["tsx", "scripts/mcp-server.ts"],
"cwd": "/path/to/domino",
"env": { "FALKORDB_URL": "redis://localhost:6379", "DOMINO_CLINIC": "riverside" }
}
}
}Interoperability
EHRs call outside decision support at the moment of prescribing through CDS Hooks 2.0 (HL7). Domino is a CDS service: GET /cds-services lists domino-order-select and domino-order-sign; a call carries the draft MedicationRequest and, as prefetch, the active medicines and conditions. Domino runs the prescriber check on the graph and answers with cards: a summary, a markdown detail, a suggestion that deletes the draft order, and override reasons that mirror its signed decisions.
A card, as the EHR receives it
{
"summary": "Benztropine may be treating a side effect of
Risperidone: a possible prescribing cascade",
"indicator": "warning",
"detail": "Benztropine treats parkinsonism (EPS), a known
side effect of Risperidone …",
"source": { "label": "Domino · prescribing cascade check" },
"suggestions": [{
"label": "Hold Benztropine and review Risperidone first",
"actions": [{ "type": "delete",
"resourceId": ["MedicationRequest/draft-1"] }]
}],
"selectionBehavior": "at-most-one",
"overrideReasons": [
{ "code": "intentional" }, { "code": "not-a-cascade" },
{ "code": "will-review" }
]
}Security
| A01 Broken Access Control | Each clinic is its own FalkorDB graph, chosen on the server from the session and never from a parameter. A run is bound to one patient; agents cannot pass a patient id. Conversations are scoped to the person. Writes need same-origin requests and, when set, an access key. Only humans write decisions. |
| A02 Security Misconfiguration | Nonce-based CSP, frame-ancestors none, nosniff, strict referrer policy, no x-powered-by, HSTS in production. |
| A03 Software Supply Chain | Exact-pinned dependencies, lockfile, zero npm audit findings, no install scripts of our own, ESLint chain dropped to remove a vulnerable transitive dependency. |
| A04 Cryptographic Failures | HMAC-SHA256 signatures on handoffs, tasks and decisions with timing-safe comparison; secrets only in server env. |
| A05 Injection | No string-built Cypher anywhere: 105 named, parameterised queries. Free text reaches the full-text index only as plain lower-case words (no search operators). Id lookups use null-prototype maps. LLM output is never executed as code or Cypher, and React escapes all text. |
| A06 Insecure Design | Human approval on every action, per-run write limits, a wording guardrail that rejects instructions to change medicines. |
| A07 Authentication Failures | Optional access key exchanged for an httpOnly, SameSite=Strict cookie; constant-time comparison. |
| A08 Data Integrity Failures | Signed records are verified on read; a forged decision or task is ignored and flagged. |
| A09 Logging & Alerting | Every agent step is a Step node linked to the catalog queries it ran and to what it wrote: a provenance graph you can query. Server logs omit secrets and patient data. |
| A10 Exceptional Conditions | Typed errors mapped to safe HTTP responses, timeouts on LLM calls, model fallbacks, a planner when no LLM is set. |
| LLM01 / ASI01 Prompt injection, goal hijack | Record content is wrapped as data; tools never take free text into Cypher; agent goals are fixed by role prompts and tool subsets. |
| LLM06 / ASI02 Excessive agency, tool misuse | Least privilege per agent (Detective cannot draft notes, Scribe cannot read other patients), read tools are database-enforced read-only. |
| ASI06 Memory poisoning | Only people write decisions, every decision is signed (verdict and reason) and verified before an agent uses it. Procedures change only from decided tasks, by a plain rule, and link to the tasks they learned from. |
| ASI07 Insecure inter-agent communication | Handoffs are stored in FalkorDB, signed, read back and verified; a failed check stops the run. |
| LLM09 / ASI09 Misinformation, trust exploitation | Every claim carries its graph path and evidence label; the UI shows the Cypher; outputs are questions, not orders. |
| LLM10 Unbounded consumption | Rate limits per client and bucket, step and token budgets per agent, a bounded conversation history, timeouts, one run per patient at a time across every instance (a graph lease), and a cap on open check-ups. |
| ASI03 Identity and privilege abuse | Tenant isolation by graph; the clinic-wide brain is read-only and confined to the session’s clinic graph; the MCP server serves one clinic per process. |
The knowledge graph (116 drugs, 50 effects, 268 CAUSES and 189 TREATED_BY edges, 27 documented patterns, 4 dangerous combinations) was curated by hand from product labelling, the AGS Beers Criteria 2023, the Anticholinergic Cognitive Burden scale, ThinkCascades and published studies, with references stored on each pattern. Patients are synthetic, produced by a seeded generator in this repository. AI coding assistants were used while building Domino; the team reviewed, tested and can explain every part of it.