Skip to Content
ToolsMedicine

Medicine

The medicine tool (ctx.MEDICINE) is a built-in tool available inside any loop. It answers three questions about a medicine from public data: what it is and what its label warns about, whether two medicines mention each other in their labels, and what it costs in South Africa.

Every method calls a public API and reads no Ollie data. Pass in a name, get public reference material back — the tool itself holds no state and stores nothing.

What this tool is for

  • Explaining what a medicine is for, in the label’s own terms.
  • Checking a medicine someone just mentioned against the ones you already know they take.
  • Finding a cheaper generic, or answering “how much is this?” for a South African member.

What this tool is not for

  • Not prescribing. No dosing advice beyond what the label says, and no adjusting a dose.
  • Not a graded interaction database. See the warning under checkInteractions below — this is the most important limitation in this page.
  • Not a stock or benefit check. Single Exit Price is a regulated price reference. It says nothing about whether a pharmacy has the medicine, or what a member’s medical aid will pay.

Tool API

All methods must be called inside a step.do callback — ctx only exists there. See Step context.

type MedicineTool = { lookupReference(input: { name: string }): Promise<MedicationReferenceResult>; checkInteractions(input: { names: string[]; }): Promise<InteractionCheckResult>; searchSouthAfricanPrice(input: { name: string; nappiCode?: string; }): Promise<SouthAfricanMedicinePriceResult>; };
MethodDescriptionSources
lookupReferenceNormalised names, DailyMed label pointers, and openFDA label sections (uses, warnings, do-not-use, ask-a-doctor).RxTerms, DailyMed, openFDA
checkInteractionsWhether each medicine’s own label names any of the others, with the sentence quoted. Read the limitations below.RxNorm, openFDA labels
searchSouthAfricanPriceRegulated SEP price, NAPPI code, dosage form, and generic alternatives.medicineprices.org.za

lookupReference

const label = await step.do("Look up medicine", async ({ ctx }) => ctx.MEDICINE.lookupReference({ name: "ibuprofen" }), ); // label.normalizedNames -> ["Ibuprofen (Oral Pill)", ...] // label.dailyMed.labels -> [{ title, setId, publishedDate, url }, ...] // label.openFda.uses -> label indications text // label.openFda.warnings -> label warnings text

dailyMed and openFda are each either a result object or { error }. The three upstream services are queried in parallel, so one being slow or down degrades that key only — you’ll still get whatever the other two returned. Long label prose is truncated so a single result can’t blow a model’s context.

checkInteractions

An empty findings array is not evidence that a combination is safe. This method reads each medicine’s own FDA label and reports whether it names any of the others. It is not a curated, severity-graded interaction database.

NLM retired the RxNav drug-interaction API on 2 January 2024 with no replacement, and the remaining graded datasets (DrugBank, DDInter) all require a licence or an API key. So the honest primitive this tool can ship is label evidence: drug A’s label names drug B, and here is the sentence.

Three consequences to design around:

  1. Findings are directional. Warfarin’s label names aspirin; aspirin’s label may not name warfarin. You may get one finding for a pair, or two, or none.
  2. Findings are ungraded. There is no severity field. A mention might be a contraindication or a passing note — quote the excerpt rather than characterising it yourself.
  3. unchecked is not findings: []. Anything whose label couldn’t be retrieved (local brand names like “Panado”, OTC monograph products with no interactions section) lands in unchecked. For those, the answer is “I couldn’t check this one”, not “this is fine”.
const check = await step.do("Check interactions", async ({ ctx }) => ctx.MEDICINE.checkInteractions({ names: ["warfarin", "aspirin"] }), ); // check.findings -> [{ medicine: "warfarin", interactsWith: "Aspirin", excerpt: "...Antiplatelet Agents aspirin, cilostazol..." }] // check.unchecked -> [] (names whose label we could not fetch)
type InteractionCheckResult = { query: string[]; resolved: { query: string; match: { rxcui: string; name: string } | null }[]; findings: { medicine: string; interactsWith: string; excerpt: string; }[]; unchecked: string[]; source: string; limitations: string; safety: string; };

Names are normalised through RxNorm first, so a brand name and its generic resolve to one concept before the labels are searched. Both the RxNorm name and the raw string you passed are used as aliases, which is what lets a local brand name still match if it appears in a label verbatim.

The method throws if you pass fewer than two distinct names; it caps at six.

searchSouthAfricanPrice

const prices = await step.do("Find prices", async ({ ctx }) => ctx.MEDICINE.searchSouthAfricanPrice({ name: "panado" }), ); // Then pull generics for a specific product: const generics = await step.do("Find generics", async ({ ctx }) => ctx.MEDICINE.searchSouthAfricanPrice({ name: "panado", nappiCode: prices.matches[0].nappiCode, }), );

genericAlternatives is only populated when you pass a nappiCode — the two-step shape mirrors the upstream API, which needs a specific product before it can list what’s equivalent to it.

Example: file a medicine and check it against the others

import { Workflow } from "./loop"; export class MedicineLoop extends Workflow { async run( event: HealthEvent<{ patientId: string; medicine: string }>, step: HealthStep, ) { const user = await step.do("Identify patient", async ({ ctx }) => ctx.USER.getUser({ id: event.body.patientId }), ); const known = await step.do("Read what they take", async ({ ctx }) => { const metadata = await ctx.USER.getUser({ id: event.body.patientId }); return ((await metadata.getMetadata()).medicines ?? []) as string[]; }); const [label, interactions] = await Promise.all([ step.do("Look up the label", async ({ ctx }) => ctx.MEDICINE.lookupReference({ name: event.body.medicine }), ), known.length > 0 ? step.do("Check interactions", async ({ ctx }) => ctx.MEDICINE.checkInteractions({ names: [event.body.medicine, ...known], }), ) : null, ]); await step.do("Remember it", async () => { await user.remember(`Takes ${event.body.medicine}`); }); return { uses: "openFda" in label && "uses" in label.openFda ? label.openFda.uses : null, // Quote findings; never characterise severity yourself. findings: interactions?.findings ?? [], // Say plainly which ones you could not check. unchecked: interactions?.unchecked ?? [], }; } }

PHI posture

The tool makes outbound requests to public APIs with only a medicine name in the query string. It reads and writes no Ollie data.

The medicine name is still the member’s data, though — a request to openFDA for “warfarin” leaks the fact that someone asked about warfarin to a third party, and the name lands in your loop’s step.do row like any other step return value. Treat it with the same care as the rest of the run.

Sources and licences

SourceWhat it providesAccess
NIH RxNorm / RxTermsName normalisation, RxCUI resolutionPublic, no key. Rate-limited by terms of service
DailyMedSPL label pointersPublic, no key
openFDALabel sections including interactionsPublic, no key. Rate-limited per IP
medicineprices.org.zaSouth African SEP registry mirrorPublic, no key
Last updated on