Food
The food tool (ctx.FOOD) is a built-in tool available inside any loop. It looks up packaged food in Open Food Facts — either exactly by barcode, or by the name printed on the label — and returns calories, macros, allergens, and a Nutri-Score where the product carries one.
Each result includes a read field: one or two plain sentences your loop can send as-is, so a food-logging loop doesn’t have to assemble prose from the numbers.
What this tool is for
- Reading a product’s nutrition when a member sends a barcode or a photo of a label.
- Logging what someone ate against real figures rather than a model’s estimate.
- Surfacing allergens that matter to a specific member.
What this tool is not for
- Not home-cooked food. Open Food Facts catalogues packaged products. A plate of rice and chicken isn’t in it, and no amount of searching will find it.
- Not a complete database. Coverage is crowd-sourced and uneven, strongest in Europe. A missing product is common and expected.
- Not a dietary recommendation. Numbers and allergens only; whether a food suits a member is a clinical judgement this tool doesn’t make.
Tool API
Must be called inside a step.do callback — ctx only exists there. See Step context.
type FoodNutrients = {
kcal: number | null;
protein: number | null;
carbs: number | null;
sugar: number | null;
fat: number | null;
saturatedFat: number | null;
fibre: number | null;
salt: number | null;
};
type FoodFacts = {
barcode: string | null;
name: string;
brand: string | null;
servingSize: string | null;
per100g: FoodNutrients | null;
perServing: FoodNutrients | null;
allergens: string[];
/** Nutri-Score grade A–E, where the product carries one. */
nutriScore: string | null;
/** One or two plain sentences, ready to send as-is. */
read: string;
};
type FoodTool = {
lookupBarcode(input: { barcode: string }): Promise<FoodFacts | null>;
search(input: { name: string }): Promise<FoodFacts[]>;
};| Method | Description |
|---|---|
lookupBarcode | Exact product by barcode digits. null when the barcode isn’t in the database. |
search | Up to three products matching a label name, best match first. Empty array when nothing matches. |
lookupBarcode throws if the string isn’t 6–14 digits, and search throws on a name shorter than two characters — both are programming errors rather than lookup failures. Genuine misses return null / [].
Individual nutrient fields are null when the product’s entry doesn’t carry them, and per100g / perServing are null when nothing at all was recorded at that granularity. Check read first — it already handles the missing-data cases in prose.
Matching honestly
search matches on name, so a search for "chicken" returns packaged chicken products. Before you trust a hit, confirm its name is plausibly the thing the member actually has. When nothing matches, or the match is clearly wrong, the right move is to estimate and say it’s an estimate — not to present a near-miss product’s figures as fact.
Example: log a meal
import { Workflow } from "./loop";
export class FoodLoop extends Workflow {
async run(
event: HealthEvent<{ patientId: string; barcode?: string; food?: string }>,
step: HealthStep,
) {
const facts = await step.do("Read the label", async ({ ctx }) => {
if (event.body.barcode) {
return ctx.FOOD.lookupBarcode({ barcode: event.body.barcode });
}
if (!event.body.food) return null;
const [match] = await ctx.FOOD.search({ name: event.body.food });
return match ?? null;
});
await step.do("Remember the meal", async ({ ctx }) => {
const user = await ctx.USER.getUser({ id: event.body.patientId });
await user.remember(`Ate ${facts?.name ?? event.body.food}`);
});
if (!facts) {
return {
matched: false,
// No product on file — anything the loop says next is an estimate.
guidance:
"Estimate from the photo and say plainly that it is an estimate.",
};
}
return {
matched: true,
read: facts.read,
kcalPer100g: facts.per100g?.kcal ?? null,
allergens: facts.allergens,
nutriScore: facts.nutriScore,
};
}
}PHI posture
The tool sends a barcode or a food name to Open Food Facts and reads no Ollie data. What a member eats is their data, though — it lands in your step row, and in user.remember(...) if you choose to store it.
Source
Open Food Facts, an open database under the Open Database License . Public, no API key. Product data is contributed by the public and may be incomplete or out of date.