Dynamic AI Form Generation
Jan 3, 2026 • 18 min read
Sometimes the best UI is a form. "Book me a flight." → The LLM shouldn't just say "Done". It should present a Confirmation Form ("Please review these details") before execution.
1. The "Human-in-the-Loop" Pattern
- User: "Create a Jira ticket for this bug."
- LLM: "I will generate a ticket. Please confirm the details."
- UI Renders: A form with "Title", "Description", "Priority" pre-filled.
- User: Edits "Priority" from Medium to High. Hits "Submit".
- Server Action: `createJiraTicket(formData)` is called.
2. Implementation with React Hook Form
Don't reinvent the wheel. Use react-hook-form and zod. The layout is static, but the values are generative.
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// 1. Define the Schema (Shared between Client & AI)
const TicketSchema = z.object({
title: z.string().min(5, "Title too short"),
description: z.string(),
priority: z.enum(['Low', 'Medium', 'High']),
email: z.string().email()
});
export function TicketForm({ defaultValues, action }) {
const form = useForm({
resolver: zodResolver(TicketSchema),
defaultValues // <--- Populated by LLM
});
return (
<form onSubmit={form.handleSubmit(action)} className="space-y-4">
<div>
<label>Title</label>
<input {...form.register('title')} className="dark-input" />
{form.formState.errors.title && <p className="text-red-500">{form.formState.errors.title.message}</p>}
</div>
<div>
<label>Priority</label>
<select {...form.register('priority')} className="dark-select">
<option>Low</option>
<option>Medium</option>
<option>High</option>
</select>
</div>
<button type="submit" className="btn-primary" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Creating...' : 'Create Ticket'}
</button>
</form>
);
}3. The Server Side (Generating the Form)
How does the AI know to render this?
// actions.tsx (Tool Definition)
generate: async function* ({ ...args }) {
// args = { title: "Fix bug", priority: "High" } inferred from chat
yield <BotCard>Generating form...</BotCard>;
return (
<BotCard>
<TicketForm
defaultValues={args}
action={async (data) => {
'use server';
// 1. RE-VALIDATE ON SERVER (Security)
const validData = TicketSchema.parse(data);
// 2. EXECUTE
await jira.issues.create(validData);
return <div className="text-green-400">Ticket Created!</div>;
}}
/>
</BotCard>
);
}4. Advanced: Streaming Partial Forms
What if the form is HUGE? You don't want to wait for the whole JSON.
New Vercel AI SDK Feature: useObject allows you to stream a structured object to the client.
This allows you to verify the title while the description is still generating.
import { experimental_useObject as useObject } from 'ai/react';
const { object, submit } = useObject({
api: '/api/generate-ticket',
schema: TicketSchema,
});
// The input field updates in realtime as 'object.title' streams in!
<input value={object?.title} />5. Dynamic Dependent Fields
Sometimes the form structure itself needs to change. Use Zod Discriminated Unions.
If type == "Car", show "License Plate".
If type == "Boat", show "Hull ID".The LLM is excellent at respecting these conditional types if you export the Zod schema to JSON Schema.
Conclusion
Forms are the ultimate interface for structured data entry. Generative AI makes them magical by pre-filling the boring parts while leaving the Human in the Loop for the critical parts.