Blog - Building Hibrido: What Happens When the Model Is Not Allowed to Name a Movement
How Everseed built Hibrido, a daily hybrid workout published in three languages, by moving the exercise vocabulary out of the prompt and into a versioned catalog the model can only choose from.
Product & Ventures
Most "AI writes your workout" products are a questionnaire, a subscription, and a plan generated for one person. We built Hibrido the other way round: one workout a day, the same one for everyone, published every morning in Spanish, English and Portuguese, free and without an account.
That decision made the interesting problem unavoidable. If everybody gets the same session and nobody can ask a question, the session has to be right the first time, at six in the morning, for a stranger.
Here is what we did about it.
"Mostly" Is the Wrong Standard
Put "no Olympic lifting, no high-skill gymnastics" in a system prompt and a good model will comply most of the time. Most of the time is fine when the failure mode is a clumsy sentence. It is not fine when the failure mode is somebody attempting a snatch alone in a commercial gym because a website told them to.
There is a second, duller failure that shows up only after a few weeks. A model that writes its own movement names writes them a little differently each night, and differently again in each language. "Sled push" becomes "sled drive", empuje de trineo becomes empujón de trineo, and readers in three languages slowly stop doing the same workout.
Both problems have the same shape. The model is being trusted with the vocabulary, and the vocabulary is exactly the part that has to be stable.
The Fix: A Catalog, Not a Prompt
So we took the vocabulary away from it.
Hibrido has a fixed catalog of 59 movements, versioned in the repository and already translated into all three languages. Each movement carries the equipment variants it is available in. The model is handed that catalog and composes a session by choosing ids from it. It never writes a movement name.
Output is validated against the catalog before it can be stored. An unknown id is rejected. A known id used in an equipment variant it is not available in is rejected. On a rejection the generator retries with the validation errors attached, three times, and then fails the run rather than storing something degraded.
The difference is not subtle. "Please avoid X" is a request. "X is not in the catalog, and anything outside the catalog does not parse" is a constraint. Only one of those still holds on the four-hundredth night.
This is the general lesson, and it survives outside fitness: whatever your product cannot afford to have invented, move it out of the prompt and into data the model selects from. The prompt is where you put the judgment. The repository is where you put the facts.
Two Calls, So Translation Cannot Drift the Training
Generation happens in two calls, and the order matters.
The first call produces the language-neutral structure: which movements, how many reps, what loads, what time domains, in which order. No prose, no coaching, no language at all beyond the ids.
The second call writes the coaching prose in all three languages against that fixed structure. It can explain how the session should feel, where to hold back and where to push. It cannot change a single number, because the numbers were decided before it ran.
Translation is usually where multilingual content quietly diverges, because each language is generated as its own artifact and reviewed by whoever happens to read that language. Splitting structure from prose removes the possibility rather than auditing for it. The Portuguese reader and the English reader are looking at the same session by construction.
The Image Model Is Told Even Less
Hibrido renders its own artwork: a 16:9 plate for the site and a 4:5 plate for social, both with the typography deliberately left out so text can be laid over them.
The image model is never told what the workout is called. It receives a physical description of the day's dominant movement, drawn from a file with one line for every catalog id. A test fails the build when a catalog id has no scene, so the two files cannot drift apart.
Two things fall out of that. The exclusions hold for the picture the way they hold for the session, since a scene only exists for a movement in the catalog. And the workout's own prose never reaches a model that would happily paint it into the corner of a frame that exists to have real text placed on top.
The people in the pictures come from a Postgres table: invented athletes, one row each, with a physical description and a reference portrait rendered from that description. Days rotate through the roster, and when a person has a portrait it goes to the image model alongside the prompt, scoped to identity only. A week of plates is recognisably the same handful of faces instead of seven strangers who happened to match seven sentences. If the table is empty or unreachable, generation falls back to a built-in list of descriptions: faceless, but the plates keep rendering.
One Stage Is Allowed to Fail
Structure and copy give up rather than degrade. The image stage does the opposite: if it fails, the failure is recorded against that stage and the workout is stored anyway.
That asymmetry is deliberate, and it is worth stating explicitly in any pipeline with several model calls in it. Ask of each stage: is a missing output worse than a wrong one? For the session, a wrong one is far worse, so it fails loudly. For the picture, a missing one costs a background image, so it fails quietly and the product still works.
Publishing Two Days Early
The nightly job runs at 08:00 UTC and generates two days ahead.
Generated workouts are stored as published immediately, which sounds reckless until you see the second rule: a date is only rendered once it is live somewhere on earth. So there is a 48-hour window in which a session exists, is reviewable in the dashboard, and is visible to nobody. A failed run, a model outage, or a session that simply reads badly all have two days to be caught and pulled.
Which date is yours is a separate question, and the home page answers it from the reader's own timezone, resolved server-side from the request. A reader in Vancouver is not handed tomorrow's session at breakfast, while anyone already into the 9th can open the 9th directly.
The Stack
Next.js 16 with React 19, server-rendering every dated page. Supabase Postgres for sessions, the cast and the signup list, with writes closed to the service role. Model calls through OpenRouter, validated with Zod. Migrations are forward-only and applied automatically on merge by CI, using the same script a developer runs locally, with destructive statements refused unless the file explicitly allows them. Deployed on Vercel, with the nightly generation on Vercel Cron.
There is no rollback path for migrations, on purpose. A down-migration running unattended is more dangerous than the problem it solves; recovery is a new forward migration, or a restore.
Where It Is Now
Deliberately early. The generator runs nightly, the site publishes in three languages, and the signup form keeps the address you give it. Nothing has been sent yet: no email, no confirmation, no unsubscribe, because none of it is built. WhatsApp and social distribution are later phases.
That order is the whole argument. The workout quality is the product; everything else is distribution for something that has to be good first.
Read today's session at hibrido.app, or see the full Hibrido case study.
