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 strict tool use or JSON structured outputs constrains the response to this shape. It guarantees a string in exerciseId; it does not prove that the ID exists in your database. Referential integrity remains an application-level validation step.
const result = await anthropic.messages.create({
model: "claude-sonnet-5",
max_tokens: 1024,
tools: [
{
name: "generate_workout",
strict: true,
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 can take several seconds, so show immediate progress and provide cancellation. Do not feed arbitrary partial JSON into the product UI: streamed structured data may be incomplete until the relevant object closes. Use a provider-supported incremental parser if you need a preview, and only persist or assign objects after the complete response passes schema and database validation.
The pattern isn't specific to workouts
Swap "workout plan" for "meeting summary," "ad copy draft," or "onboarding email" and the same decisions carry over: constrain the shape, validate against real data, ship a reviewable draft rather than a final action, and design a clear wait-and-cancel experience. Model and prompt quality matter, but the surrounding product controls determine whether the feature is safe and useful.
Official references
Continue exploring
The Claude API guide covers streaming and tool use, and the WordPress Claude integration applies the same boundaries inside a plugin. This workflow comes from Reppod; see AI integration development for implementation support.
Related articles