Grounding an LLM in Your Exercise Database
Updated July 27, 2026
You have an exercise catalogue and you want a model to build workouts out of it, not out of its imagination. The instinct is to reach for a vector database, because that is what "RAG" has come to mean. For a few-thousand-row exercise table, that instinct is usually wrong. The constraints in a workout request are categorical — this equipment, that muscle group, not this contraindicated movement — which makes them WHERE clauses, not similarity gradients. Filter in SQL, enumerate the survivors into the prompt, and have the model return IDs from that list. Then reject, server-side, anything that is not in the set you supplied. That last step is the whole point: grounding done properly makes a hallucinated exercise impossible to persist, not merely unlikely.
This page is about language models over your own catalogue. If you want the broader "how do I generate a plan at all" picture, that is AI workout plan generation; if you do not have a catalogue yet, exercise database APIs covers where to get one.
Three ways to ground a model, and what each is for
Textbook RAG exists to solve semantic retrieval over a large unstructured corpus: chunk documents, embed the chunks, store them in a vector index, embed the query, run approximate nearest-neighbour search, stuff the top results into the prompt. An exercise database is not a large unstructured corpus. It is a small, closed, well-labelled table — typically low thousands of rows with clean attributes like movement pattern, primary and secondary muscle, equipment, unilateral or bilateral, difficulty band.
That difference changes which retriever is correct.
| Strategy | How it works | Corpus it fits | Where it breaks |
|---|---|---|---|
| Enumerate and pick | Put the whole catalogue (or a stable slice) in the prompt or a schema enum; the model selects IDs from it | Very small, stable sets — a movement-pattern list, a warm-up library, a few hundred exercises | Context cost grows with catalogue size; per-user enum sets churn your schema (see below) |
| Structured filter, then pick | Hard constraints run as SQL WHERE clauses first; the 30-150 survivors go into the prompt; the model selects and sequences them | The default for an exercise catalogue: thousands of clean structured rows, categorical constraints | Needs good, complete attributes. If your rows are not tagged for equipment and contraindications, there is nothing to filter on |
| Embedding retrieval | Embed each row, embed the user's phrasing, retrieve by vector similarity | Large, messy, user-generated corpora — a food database, a library of coaching articles | Answers hard constraints badly. "No barbell" is a boolean, and a similarity search will happily return a near-miss that violates it |
| Hybrid lexical plus vector | Keyword or BM25 search fused with vector search, then filtered | Millions of rows where users type brand and dish names that match nothing lexically | More infrastructure, more tuning, more ways to be silently wrong. Overkill for a table you could enumerate |
The rule of thumb worth internalising: corpus size and structure decide the retriever, not fashion. Thousands of clean structured rows means SQL plus enumeration. Millions of messy user-generated rows means hybrid retrieval.
There is a debuggability argument too, and for a health-adjacent product it may matter more than the cost one. A filtered candidate set is something you can print, diff, log against a generated plan, and unit-test. When a user with a knee contraindication gets a leg extension, you can answer "was it in the candidate set?" with a boolean. An embedding retriever's misses are opaque by construction — the answer is "the cosine similarity was 0.71", which tells you nothing you can fix deterministically.
Where embeddings genuinely help inside an exercise catalogue is synonym resolution: "RDL", "stiff-leg deadlift", and "Romanian deadlift" pointing at one row. Even there, a curated alias table usually beats embeddings and has the advantage of being auditable by a human who knows what those lifts are.
The filter is a WHERE clause. Write it as one.
The hard constraints in a workout request are almost entirely categorical, and every one of them is a filter you already know how to write:
// Hard constraints belong in the query, not in the prose of a prompt.
const candidates = await db.exercises.findMany({
where: {
equipment: { in: user.availableEquipment },
primaryMuscle: { in: session.targetMuscles },
difficulty: { lte: user.level },
id: { notIn: user.contraindicatedExerciseIds },
},
select: { id: true, name: true, pattern: true, primaryMuscle: true },
});
This matters beyond retrieval quality. A constraint stated only in prose — "the user has no barbell, avoid barbell movements" — is a soft constraint the model can drift outside of and a user can negotiate away over a few turns (why that happens). A constraint enforced as a WHERE clause cannot be argued with, because the excluded row was never in the context in the first place. That is the retrieval layer doing safety work for free, and it is the strongest reason to prefer a filter you can read over a similarity score you cannot. Put safety-relevant exclusions in the query; put preferences in the prompt.
Making hallucination impossible, not just unlikely
Most "reduce hallucinations" advice is probabilistic: better prompts, more grounding, ask the model to cite. Provider guidance is explicit about the useful version of this — restrict the model to the supplied source material and tell it not to fall back on general knowledge, let it say it does not know, and require a supporting citation for each claim. All good practice, all a reduction in likelihood.
Set membership is different in kind. If the model's only job is to return an exercise_id, and every returned ID is checked against the exact candidate set you supplied, then an invented exercise is not a rare bad output — it is an output that cannot survive validation. You have converted an open-ended generation problem into a closed-set selection problem with a decidable validator. That is the property worth designing for.
Two mechanisms get you there, and it is worth knowing which to use:
- Structured outputs. You can constrain the response to a JSON schema so the response is guaranteed to be valid JSON matching it, and mark tool parameters strict so parameters are validated at generation time. Schemas support
enum, which is the direct mechanism for "you may only pick from this list". - The candidate list in the prompt, plus a server-side ID check. Less elegant, and usually the better engineering choice.
The reason for that preference is a cost gotcha. Compiled grammars for structured output are cached, and the cache is invalidated when the schema structure changes. A per-user filtered enum of exercise IDs is a distinct schema for every distinct user filter, so every request pays cold grammar compilation. Prefer a stable schema — exercise_id typed as a plain string — with the filtered candidates in the prompt, and enforce membership yourself. You get the same guarantee from your own set check, without churning the schema on every request.
One more trap: a refusal or a max_tokens truncation can still produce output that does not match your schema. Check the stop reason before you trust the parse.
Schema validity is not sensibility
Here is the part no provider feature does for you. Structured output guarantees that the JSON parses and matches your schema. It guarantees nothing about whether the plan is reasonable — and the schema layer cannot even express most of what "reasonable" means numerically. Numeric constraints (minimum, maximum, multipleOf), string length constraints, and most array constraints are not supported. You cannot enforce "reps between 1 and 30" or "at most 10 sets" at the decoding layer. Client-side SDK helpers will strip those constraints from the wire schema and re-check them after generation, which is exactly the point: the check happens on your side, after the fact, or not at all.
So write the validator. A minimum viable one for a generated session:
const allowed = new Set(candidates.map(e => e.id));
for (const item of plan.exercises) {
if (!allowed.has(item.exercise_id)) reject("exercise outside candidate set");
if (item.sets < 1 || item.sets > 8) reject("set count out of range");
if (item.reps < 1 || item.reps > 30) reject("rep count out of range");
if (item.rest_seconds > 600) reject("implausible rest interval");
}
if (new Set(plan.exercises.map(e => e.exercise_id)).size !== plan.exercises.length) {
reject("duplicate exercise in one session");
}
Beyond that, check cross-row invariants the per-item loop cannot see: total sets per muscle group against a weekly ceiling, estimated session duration against the user's stated time budget, and that every returned exercise still satisfies the original filter. On failure, prefer deterministic repair or a filtered retry over re-prompting the model, so a bad generation cannot loop into cost. And reject rather than repair on a set miss — a hallucinated ID is a signal that something upstream is wrong, not a typo to patch.
A useful division of labour falls out of this: let the model do selection and sequencing — which movements, in what order, with what rep-scheme intent — and let deterministic code do arithmetic: load progression, volume accounting, macro totals. Models are good at the former and unreliable at the latter, and the arithmetic is the part users notice when it is wrong.
Treat user-supplied text as untrusted while you are at it. A custom exercise name or a workout note is user-controlled content that ends up in the model's context, which makes it a prompt-injection surface. The set check protects the output; it does not sanitise the input.
Enumeration is cheaper than it sounds
The usual objection to putting candidates in the prompt is token cost. Run the arithmetic before accepting it. Take a hypothetical: 60 exercises survive your filter, each rendered as roughly 30 tokens of ID, name, pattern, and muscle. That is about 1,800 input tokens. At Claude Opus 5's input rate of $5.00 per million tokens (as of mid-2026 — verify current pricing), 1,800 tokens costs 1,800 / 1,000,000 x $5.00 = $0.009 per uncached call. Substitute your own catalogue's row size, filter selectivity, and calls per user; the shape of the calculation is what transfers, not the number.
Prompt caching then makes the stable part cheaper still — a shared catalogue slice that is identical for every user is exactly the kind of bulk a cached prefix is for, and what AI fitness features cost covers the mechanics and the break-even.
The cheapest token is still the one you do not send. Pre-filter in SQL before enumerating: do not put 3,000 exercises in the context when 60 pass the filter.
Food catalogues are the case where vector search earns it
The argument above is about a small structured table. Invert every property and you get the food database, where the same discipline points at a different retriever. Food corpora are large, user-generated, and lexically messy — a user types "chicken tikka" and your row is "Chicken Tikka Masala, restaurant-style"; brand names, regional dish names, and free-text portions match nothing exactly. That is a genuine hybrid lexical-plus-vector retrieval problem, and it is worth the infrastructure.
What does not change is the grounding contract, and that is the useful half of the comparison: swapping the retriever does not soften the validator. Retrieval resolves to a food_id, your server recomputes the macros from the catalogue row, and a returned identifier that is not in the candidate set is rejected exactly as it would be for an exercise. AI nutrition logging covers the retriever and the accuracy side.
What grounding does not fix
Grounding makes the identifiers real. It does not make the plan appropriate, and it is worth being blunt about the gap.
- A plan made entirely of real exercises can still be unsafe for this user. Set membership is a referential-integrity guarantee, not a clinical one. Someone signed off on your catalogue's form cues and contraindications; nobody signed off on the combination the model just assembled.
- Risk signals need a deterministic gate in front of the model, not a prompt inside it. A
WHEREclause excludes rows; it does not decide whether this user should be getting a plan at all. Cardiac, eating-disorder, self-harm and acute-injury signals belong to a gate that runs before retrieval, and declared conditions belong to a constrained path rather than a refusal. The trigger list and the tiering are on LLM safety for fitness advice. - Your catalogue is now your quality ceiling. Version the catalogue snapshot alongside the prompt, schema, and model. A catalogue edit can regress plan quality with no code change at all, and only a versioned snapshot makes that visible.
- Enumerating candidates means shipping user context to a third party. The contraindication list you use to build the filter is health data, and putting it in a prompt triggers Apple's Guideline 5.1.2(i) disclosure-and-explicit-permission requirement. Filtering server-side and sending only the surviving IDs is the cheaper posture in both senses. See app store health data rules.
- A "not medical advice" line is good practice and an app-store expectation. It is not a liability shield. It does not satisfy a regulator and it does not make an unsafe answer safe. Whether AI coaching providers owe a legal duty of care is, as far as we could establish, unsettled — no case law was found either way.
Start with the boring retriever
If your catalogue is a few thousand structured rows, build the boring version first: SQL filter, enumerate the survivors, model returns IDs, server checks set membership and numeric ranges, deterministic code does the arithmetic. It is cheaper, it is debuggable, and it gives you a validator that can actually say no. Reach for embeddings when the corpus stops looking like a table and starts looking like a pile of text — which, for most fitness apps, means the food database and not the exercise one.
If you are still choosing the catalogue underneath all this, exercise database APIs compares the options, including how well each one tags equipment, muscle groups, and difficulty — which is precisely what determines whether the filter-first approach works at all.
Frequently asked questions
- Do I need a vector database to ground an LLM in my exercise catalogue?
- Usually not. Vector search exists to solve semantic retrieval over large unstructured corpora. An exercise catalogue is typically low thousands of clean, structured, well-labelled rows, and the constraints in a workout request are categorical: available equipment, target muscle group, difficulty band, excluded contraindications. Those are WHERE clauses, and a similarity search answers them badly because 'no barbell' is a boolean, not a gradient. Filter in SQL, put the surviving candidates in the prompt, and let the model select from them. Embeddings earn their keep when the corpus is large, messy and user-generated, which describes a food database far more than an exercise table.
- How do I stop the model inventing exercises that are not in my database?
- Do not try to make it unlikely, make it impossible to persist. Restrict the model to returning an exercise_id, supply the exact candidate set, and check every returned ID against that set on your server before anything is written. An ID that is not in the set is rejected outright rather than repaired, because a miss usually signals an upstream problem. Prompt-level techniques help, and provider guidance recommends explicitly restricting the model to supplied source material and letting it say it does not know, but those reduce likelihood. Set membership is a decidable check, which is a different kind of guarantee.
- Can a JSON schema enforce sensible sets and reps?
- No. Structured output guarantees the response is valid JSON matching your schema, but the schema layer does not support numeric constraints such as minimum, maximum and multipleOf, nor string length limits or most array constraints. That means you cannot enforce 'reps between 1 and 30' or 'at most 10 sets' at the decoding layer. SDK helpers strip those constraints from the wire schema and re-validate after generation, which is the pattern to copy: check ranges, duplicates, weekly volume ceilings and session duration server-side. Schema validity is not physiological sensibility, and neither one makes a plan safe for a given user.
- Should the exercise IDs go in a schema enum or in the prompt?
- A schema enum is the most direct way to say 'pick only from this list', and enums are supported. The practical catch is that compiled grammars are cached and invalidated when the schema structure changes, so a per-user filtered enum is a distinct schema for every distinct user filter and pays cold compilation each time. In most apps a stable schema with exercise_id typed as a plain string, the filtered candidates supplied in the prompt, and a server-side set check gives the same guarantee without churning the schema on every request. Also check the stop reason first, since a refusal or a max_tokens truncation can produce output that does not match your schema at all.
- Does the same approach work for a food database?
- The grounding contract does, the retriever does not. Food catalogues are large, user-generated and lexically messy, so users type brand and dish names that match no row exactly, and hybrid lexical plus vector retrieval genuinely earns its complexity there. What stays the same is that retrieval should resolve to a food_id and a quantity in a canonical unit, with every macro recomputed server-side from the catalogue row. MyFitnessPal's Meal Scan and Voice Log are publicly described as resolving input to entries in an existing verified food database rather than generating nutrition numbers. Never persist macro values a model emitted.
Keep reading
Engineering guidance, last reviewed July 27, 2026. This is not medical, nutritional, or legal advice. Language models produce fluent, confident output that can still be wrong — and when the output is exercise or nutrition guidance, being wrong has consequences. Whatever your app tells a user is your responsibility, not the model vendor’s. Model behaviour, pricing, and platform AI policies change often; verify against current official documentation, and design for a qualified human in the loop wherever the advice could cause harm.
← All ai features · by AIFitnessAPI