All articles
AI

How to add AI-generated content without it feeling bolted on

Written by Farhan ShafiJuly 6, 2026Updated August 20, 20267 min read

Contents7

"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.

These are the things that made AI-generated content hold up when I added it to Reppod's workout builder. None of them are about 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 have 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) }],
});

Pass the client's real exercise library into the prompt instead of asking the model to invent exercises. That is what turns "AI-generated" from a novelty into something a trainer can 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). After generation, check the output against your real data as well as 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. Do not silently drop the invalid exercises and hope the trainer notices the plan is now incomplete.

Make it a draft, not a decision

The most useful 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 choice absorbs most of the risk of an imperfect model output. The human in the loop is a feature, not a compromise you are 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.


Farhan Shafi
WordPress and Full-Stack Developer

Related articles