nutrition-reports
Skill1 starsChanged 40 days agoMITView on GitHub
Use when the user wants a nutrition or dietitian-style evaluation of a Cooklang recipe (.cook) or meal plan (.menu) - calories, macros, micronutrients, passed/failed checks against targets, exclusions/allergens, a plan's per-day nutrition - or wants to SCREEN recipes that already meet a number ("which of my recipes are over 35% protein?"). Not for changing a recipe or plan to hit targets - that is nutrition-goals.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- cooklang/cooklang-skillsword for word, first seen Oct 13, 2026
Other versions1 project uses a version of this.
Word for word · 1
- cooklang/cooklang-skills★ 9100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository13 other files it gives its agents.
- cooklang-editingSkill
- cooklang-validationSkill
- export-recipeSkill
- meal-planningSkill
- metadataSkill
- nutrition-goalsSkill
- organize-collectionSkill
- pantrySkill
- recipe-importSkill
- recipe-searchSkill
- report-authoringSkill
- scale-recipeSkill
- shopping-listSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 1 is distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 10
Use when the user wants a nutrition or dietitian-style evaluation of a recipe or meal plan — calories/macros/micronutrients, passed/failed …
- Very specific: line 114
Do not call nutrition_for(ingredient) in a loop over ingredients: it raises a template error on the first unresolved ingredient or …
- Very specific: line 117
Do not add your own "coverage must be 100 %" checks or {% raise %}-style guards.
- Distinctive: line 121
Never present partial totals as complete.
- Very specific: line 203
Check macros — {% import "ck" as ck %}, then each macro records a check and renders a line:
- Very specific: line 259
When the user wants nutrients framed against a daily target, use dv_percent (or reference_intake for the raw figure) instead of inventing …
- Very specific: line 286
"authentication required": call login, show the user the code and link, retry once they've approved.
- Very specific: line 294
When the user wants to keep the report, save it as a .jinja file:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Skill: Nutrition Reports
Needs the Cook MCP server (tools like render_report and read_recipe). If these tools are missing, the Cook MCP server isn't connected: if you installed the cooklang plugin or extension, check that its cook server is running (e.g. /mcp) or reinstall it; otherwise add it from https://github.com/cook-md/cook-mcp. Never estimate nutrition from your own knowledge instead.
Use when the user wants a nutrition or dietitian-style evaluation of a recipe or
meal plan — calories/macros/micronutrients, passed/failed checks against targets, exclusions
or allergens, or aggregating nutrition across a .menu plan. Backed by the Cook
nutrition service, called through the render_report tool.
This skill is for nutrition specifically. For plain counts, costs (db() datastore) and
custom printouts, follow the report-authoring skill; its aisled() /
excluding_pantry() / db() functions work inside nutrition templates too.
Access
Nutrition data needs a cook.md login and a Cook Basic or Cook Pro plan. If a render
fails with "authentication required", call login and show the user the code and link,
then retry. If it fails with "subscription required", show the user the pricing link from
the error. The direct nutrition tools (get_nutrition, aggregate_nutrition, lookup_ingredient, …) return JSON login_required (call login) or plan_required (show the user checkout_url) instead. auth_status tells you whether the user is logged in and on which plan.
How rendering works
Nutrition reports are Jinja2 templates rendered with render_report (it never edits
files). The template calls nutrition functions (below); the service resolves each
ingredient. render_report returns { rendered, checks, resolve_failures }:
checks are the passed/failed checks the template recorded, resolve_failures the
ingredients the service couldn't resolve, each with an error code, message and
suggestions.
- Reuse first: saved templates live in
reports/(CookCLI) orconfig/reports/(Cook Editor);list_recipeskind: "template"lists both. If one answers the question, check its inline targets/exclusions match the user's, then render it withtemplate_path. If the targets differ, author inline (or offer to update the saved template). - Otherwise draft inline (
template) and iterate: readrendered, fix errors, repeat. input_pathis the.cook/.menufile aslist_recipesreturned it (e.g."plans/Week of May 18.menu").scaledefaults to 1 and scales.cookrecipes only.- Targets and exclusions: define them inline in the template, or point
client_profile_pathat a profile YAML in the collection. The profile is then available to the template asclient:
name: Sara
exclusions: [peanut, shellfish]
targets:
energy_kcal: { min: 1800, max: 2200 }
protein_g: { min: 75 }
sat_fat_g: { max: 20, tol_pct: 10 }Fixing resolve failures
ingredient_not_found→ trylookup_ingredientwith the name and retry with a suggestion, or fix the ingredient name in the recipe. Do not bake qualifiers into names —fresh garlicshould be@garlic{}(fresh).density_unavailable/ unit errors → probe withconvert_units; prefer mass units in the recipe when density is missing.- Fixing the recipe itself is an edit: follow the cooklang-editing skill, and ask first.
Screening a shortlist
"Which of my recipes are over 35% protein?", "find three quick dinners with X" —
you are filtering existing files, not evaluating one. render_report takes ONE
recipe per call, so a careless screen costs one render per candidate and turns a
three-dinner question into a hundred tool calls. Work in three passes:
- Narrow before you measure.
search_recipes(withtagwhere it helps), folder names, and the pantry tools cut the field on the cheap criteria first — course, time, ingredients on hand, cuisine. Nutrition is the last filter applied, never the first. - Cap the candidates at about six, chosen from that shortlist. If six is not enough, say so and screen a second batch — do not pre-emptively measure twenty files.
- Render, do not read. A render already resolves the recipe's ingredients and macros; reading a file you are about to render buys nothing. Read a recipe only when you need its prose — method, equipment, wording you will quote. Send the same small template for each candidate (in parallel if your client allows).
Emit one machine-readable line per candidate (NAME | kcal | protein_g |
pct_protein | OK/FAILED) so each output is a single line. Present the survivors,
then hand off — meal-planning to build a .menu from them, nutrition-goals only
if a chosen recipe has to be changed to clear the bar.
Presenting results
The rendered report is the answer: show it to the user as it came back, then add
at most three lines — the verdict, blockers (FAILED or low-confidence ingredients),
the next step. Do not retype or recompute its numbers. For .menu plans the per-day
table (each day vs. its targets) lives in the report; the summary names the days that
miss which targets. When the user supplied targets earlier in the conversation, use
them — do not ask again; with no user targets, frame against daily values
(dv_percent) rather than inventing thresholds. A single-number question may be
answered in one line from the render. If the user wants the recipe or plan changed
to hit the targets, follow nutrition-goals.
Incomplete coverage — never fail the report
The nutrition database does not resolve every ingredient (regional names, brands, unusual units). A report must still render for the ingredients it can resolve, skip the rest, and list them at the end:
- Build per-ingredient tables and totals from
aggregate_nutrition(ingredients). Itsitemsare the resolved ingredients (.ingredient,.amount.value/.amount.unit,.macros.kcal|protein_g|fat_g|carb_g|fiber_g|sat_fat_g|sugar_g,.confidence) and itsfailuresthe unresolved ones (.ingredient,.error.message,.error.suggestions).totals— and themacros()/total_calories()shortcuts — already exclude failures;totals.is_partial,totals.included_countandtotals.failed_counttell you whether they are complete. - Do not call
nutrition_for(ingredient)in a loop overingredients: it raises a template error on the first unresolved ingredient or non-numeric quantity ("some", "1–2") and aborts the whole render. Iterateagg.items. - Do not add your own "coverage must be 100 %" checks or
{% raise %}-style guards; partial data is reported, not refused. - End every nutrition report with a "Not included" section listing each failure and its reason, and say in your summary how many ingredients were skipped. Never present partial totals as complete.
{% set agg = aggregate_nutrition(ingredients) %}
{% set servings = metadata.servings | default(1) | int %}
# Nutrition — {{ metadata.title | default("Recipe") }}
| Ingredient | Amount | kcal | Protein | Carbs | Fat |
|---|---|---|---|---|---|
{% for it in agg.items -%}
| {{ it.ingredient }} | {{ it.amount.value }} {{ it.amount.unit }} | {{ it.macros.kcal | round }} | {{ it.macros.protein_g | round(1) }} g | {{ it.macros.carb_g | round(1) }} g | {{ it.macros.fat_g | round(1) }} g |
{% endfor %}
**Per serving ({{ servings }}):** {{ (agg.totals.macros.kcal / servings) | round }} kcal ·
{{ (agg.totals.macros.protein_g / servings) | round(1) }} g protein ·
{{ (agg.totals.macros.carb_g / servings) | round(1) }} g carbs ·
{{ (agg.totals.macros.fat_g / servings) | round(1) }} g fat
{% if agg.totals.is_partial %}
*Totals cover {{ agg.totals.included_count }} of {{ agg.totals.included_count + agg.totals.failed_count }} ingredients.*
### Not included ({{ agg.failures | length }})
{% for f in agg.failures -%}
- {{ f.ingredient }} — {{ f.error.message }}{% if f.error.suggestions %} (try: {{ f.error.suggestions | join(", ") }}){% endif %}
{% endfor %}
{% endif %}Meal plans (.menu)
A .menu is read through plan, not ingredients: plan.days (each with .name,
.date, .meals and the day's scaled .ingredients), plan.all_ingredients,
plan.servings (people eating, from the menu's frontmatter) and
plan.missing_recipes (references that didn't resolve — report them). Per-day totals:
{% set people = plan.servings | default(1) %}
| Day | kcal / person | Protein / person |
|---|---|---|
{% for day in plan.days -%}
{% set t = aggregate_nutrition(day.ingredients).totals.macros %}
| {{ day.name or day.date }} | {{ (t.kcal / people) | round }} | {{ (t.protein_g / people) | round(1) }} g |
{% endfor %}Non-English recipes
The nutrition database currently resolves English ingredient names; other
languages are not translated yet. When the failures are mostly non-English names
(e.g. Dutch havermout, volkorenbrood), still render the report as above and
tell the user in your summary, once:
Nutrition lookups currently understand English ingredient names —
<language>isn't supported yet, so those ingredients are listed under "Not included". If you'd like your language added sooner, email support@cook.md.
You may offer to re-run with English equivalents (nutrition_for_amount("rolled
oats", 40, "g")) for the skipped rows, clearly marked as translated by you —
it errors too if the English name is unknown, so drop that row rather than retry.
Function cheatsheet
Nutrition functions (full reference at https://nutrition.cook.md/docs/guides/functions):
| Function | Returns |
nutrition_for(ingredient) | macros for one ingredient: kcal, protein_g, fat_g, carb_g, fiber_g |
nutrition_for_amount(name, amount, unit, prep, standard?) | full item incl. micros + vitamins (no ingredient object needed); standard? ("fda"/"eu"/"uk") controls which RDA standard the response's reference_intakes table uses |
aggregate_nutrition(ingredients) | { items, failures, totals, confidence_breakdown } for a whole list — resolved rows, skipped rows, and totals that exclude the skipped ones |
macros(ingredients) | totals {kcal, protein_g, fat_g, sat_fat_g, carb_g, fiber_g, …} |
total_calories(ingredients) | number |
vitamins(ingredients) | vitamins object |
nutrient_total(ingredients, key) | number; looks in micros then vitamins ("iron_mg", "vit_d_iu") |
reference_intake(name, standard?) | daily reference intake (RDA/DV) for a nutrient key, e.g. reference_intake("sodium_mg") → 2300; undefined if the nutrient has no DV. standard? defaults to "fda" ("eu"/"uk" also) |
dv_percent(name, amount, standard?) | amount as a % of that nutrient's daily reference, e.g. dv_percent("sodium_mg", 230) → 10; undefined if no DV. standard? defaults to "fda" |
is_in_category(ingredient, slug) | bool, tree-aware (oily_fish ⇒ fish_and_shellfish) |
category_servings(plan, slug) | int — meals containing ≥1 ingredient in the category |
convert(amount, from, to, ingredient?) | unit conversion (ingredient needed for volume/count) |
compare(actual, target, op) | bool — op is gte/lte/eq |
within_tol(actual, target, tol_pct) | bool — within tol_pct % of target |
record_check(label, ok) / all_checks() / failed_checks() | record and read passed/failed checks |
matched_exclusions(ingredients, exclusions) / unresolved_exclusions(...) | exclusion resolution |
Check macros — {% import "ck" as ck %}, then each macro records a check and renders a line:
| Macro | Asserts |
ck.min(value, target, label) | value ≥ target.min |
ck.max(value, target, label) | value ≤ target.max |
ck.range(value, target, label) | target.min ≤ value ≤ target.max |
ck.within(value, target, label) | value within target.tol_pct % of target.max |
ck.between(value, lo, hi, label) | lo ≤ value ≤ hi (scalar bounds) |
ck.absent(ingredient) | the excluded ingredient is not present |
ck.min/max/range/within take a target object ({min, max, tol_pct}); ck.between
takes scalar bounds. Recorded checks also come back in render_report's checks.
category_servings and plan-wide aggregation are covered at
https://nutrition.cook.md/docs/guides/plan; provenance/estimate quality at
https://nutrition.cook.md/docs/guides/confidence.
For a quick single-ingredient question ("how much protein in 200 g tofu?") you do not
need a template: get_nutrition answers it directly, and aggregate_nutrition the
tool sums a hand-made list.
Worked template (inline targets)
{% import "ck" as ck %}
{# Targets inline; or set client_profile_path and use client.targets. #}
{% set targets = {
"energy_kcal": {"min": 1800, "max": 2200},
"protein_g": {"min": 75},
"sat_fat_g": {"max": 20},
"fiber_g": {"min": 25}
} %}
{% set exclusions = ["peanut", "shellfish"] %}
# Nutrition check — {{ metadata.title | default("Recipe") }}
{% set t = macros(ingredients) %}
- Energy: {{ t.kcal | round }} kcal
- Protein: {{ t.protein_g | round }} g
- Saturated fat: {{ t.sat_fat_g | round(1) }} g
- Fiber: {{ t.fiber_g | round(1) }} g
## Checks
{{ ck.range(t.kcal, targets.energy_kcal, "Daily kcal") }}
{{ ck.min(t.protein_g, targets.protein_g, "Protein (g)") }}
{{ ck.max(t.sat_fat_g, targets.sat_fat_g, "Saturated fat (g)") }}
{{ ck.min(t.fiber_g, targets.fiber_g, "Fiber (g)") }}
{% for ex in exclusions %}{{ ck.absent(ex) }}
{% endfor %}
{% set checks = all_checks() %}
**Summary:** {{ checks | selectattr("ok") | list | length }} of {{ checks | length }} checks passed.Showing % daily value
When the user wants nutrients framed against a daily target, use dv_percent (or
reference_intake for the raw figure) instead of inventing reference numbers. The
standard defaults to "fda"; use "eu"/"uk" for European labels. A nutrient with
no published DV returns undefined — pre-assign the result and guard with is defined
so the row is skipped (piping undefined through | round raises a template error):
{% set t = macros(ingredients) %}
{% set sodium = nutrient_total(ingredients, "sodium_mg") %}
{% set sodium_dv = dv_percent("sodium_mg", sodium) %}
{% set fiber_dv = dv_percent("fiber_g", t.fiber_g) %}
{% if sodium_dv is defined %}
- Sodium: {{ sodium | round }} mg ({{ sodium_dv | round }}% DV)
{% endif %}
{% if fiber_dv is defined %}
- Fiber: {{ t.fiber_g | round(1) }} g ({{ fiber_dv | round }}% DV)
{% endif %}Workflow: reuse a saved template by template_path if one fits → else draft inline →
render_report → read rendered / resolve_failures / fix errors → repeat → show the
report once → ≤3-line summary.
When the render errors
render_report returns an error result on failure. Read it:
- "authentication required": call
login, show the user the code and link, retry once they've approved. - "subscription required": nutrition data needs Cook Basic or Cook Pro — show the user the pricing link from the error. Do not keep retrying.
- Template error (minijinja reports the line): fix the template and render again.
Saving a reusable template
When the user wants to keep the report, save it as a .jinja file:
- Save it where the collection already keeps templates (
reports/for CookCLI,config/reports/for Cook Editor); defaultreports/. - Encode the output format in the inner extension:
nutrition.md.jinja→ markdown. - Save it with
write_config(pathe.g.reports/nutrition.md.jinja) and tell the user where. - From then on, render it with
template_pathrather than re-sending the source.
Render inline for one-off questions; offer once to save a template, and save only if the user accepts.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

