Adding AI-generated content to a product without it feeling bolted on
"Now with AI-generated workout plans" is one line of marketing copy and, if you build it the way most tutorials show, one deeply frustrating feature in production. The model happily invents an exercise that doesn't exist in your database, returns a plan with eleven sets when your UI only has room for six, or drifts format halfway through a long response. None of that shows up in a demo with three cherry-picked prompts. All of it shows up within a day of real users.
Here's what actually made AI-generated content hold up when I added it to Reppod's workout builder — the parts that have nothing to do with prompt engineering.
Constrain the output shape before you trust it
Free-text generation is the easiest thing to demo and the worst thing to build on. The model should return structured data that maps directly onto what your database and UI already expect — not prose you then try to parse:
const workoutPlanSchema = z.object({
name: z.string(),
exercises: z.array(
z.object({
exerciseId: z.string(), // must match an ID in your own exercise library
sets: z.number().int().min(1).max(8),
reps: z.number().int().min(1).max(30),
restSeconds: z.number().int().min(15).max(300),
}),
),
});
Using the Anthropic SDK's tool use (or the equivalent structured-output mode on other providers), the model is constrained to fill in this shape — it can't return an exercise your app can't render, because exerciseId has to resolve against your own library, not whatever the model imagines.
const result = await anthropic.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
tools: [{ name: "generate_workout", input_schema: zodToJsonSchema(workoutPlanSchema) }],
tool_choice: { type: "tool", name: "generate_workout" },
messages: [{ role: "user", content: buildPrompt(clientGoals, availableExercises) }],
});
Passing the client's actual exercise library into the prompt — not asking the model to invent exercises — is what turns "AI-generated" from a novelty into something a trainer can actually assign without checking every line first.
Validate, don't trust, even with structured output
Structured output reduces the failure modes; it doesn't eliminate them. A model can still return exerciseId: "ex_042" that doesn't exist in this particular client's available exercises, or an internally inconsistent plan (functionally a leg day with zero leg exercises). Validate against your actual data after generation, not just against the schema:
function validatePlan(plan: WorkoutPlan, availableIds: Set<string>) {
const invalid = plan.exercises.filter((e) => !availableIds.has(e.exerciseId));
if (invalid.length > 0) {
throw new PlanValidationError(`Unknown exercises: ${invalid.map((e) => e.exerciseId)}`);
}
}
On failure, the honest move is a clear retry with the error fed back to the model, or a fallback to a template plan — not silently dropping the invalid exercises and hoping the trainer notices the plan is now incomplete.
Make it a draft, not a decision
The single highest-leverage product decision was framing: AI-generated plans land as an editable draft assigned to no one yet, not as something auto-sent to a client. The trainer sees it, adjusts sets or swaps an exercise they don't like, and only then assigns it. That one framing choice absorbs most of the risk of an imperfect model output — the human in the loop is a feature, not a compromise you're waiting to remove.
This matters more than any prompt tweak. A slightly worse model with a mandatory review step produces a better product experience than a great model whose output goes straight to production.
Stream it, and say why it's slow
Generating a full multi-week program takes several seconds — long enough that an unstreamed response feels broken. Streaming the plan as it's built (exercise by exercise, rendered incrementally in the UI) does two things: it makes the wait feel shorter, and it gives the trainer a natural point to interrupt if the direction is obviously wrong early on, instead of waiting out a full generation that's already off track.
The pattern isn't specific to workouts
Swap "workout plan" for "meeting summary," "ad copy draft," or "onboarding email" and the same four decisions carry over: constrain the output to a schema your app can render safely, validate against your real data even after that, ship it as a reviewable draft rather than a final action, and stream so the wait itself doesn't feel like a bug. The model choice and the prompt are the easy 20% of shipping an AI feature. This is the other 80%.