Why pre-fill matters
The fastest UX is one where the user confirms instead of types. Every field your backend already knows (name, email, account tier, last order date) should arrive in the form pre-populated.
| Method | When the value is set | Source of truth |
|---|---|---|
1. default_value on the field | At render time, before the agent does anything | Hardcoded in form definition |
2. Session metadata + system prompt | Agent fills from metadata when opening form | Your database, passed at session start |
| 3. Agent tool arguments | Agent fills from conversation context | What the user said + metadata |
| 4. Direct data-channel publish | Your code publishes a form.{id} message with values | Your app at any point |
Use them in combination — default_value for static stuff, metadata for known user info, tool args for conversation-derived values, direct publish for custom flows.
Method 1 — default_value on the field
Set it on the form definition itself. The widget pre-fills before the agent does anything.
curl -X PATCH https://api.oshara.ai/api/ai-characters/support-bot/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"widget_appearance": {
"forms": [
{
"id": "feedback",
"title": "Quick feedback",
"submit_url": null,
"fields": [
{ "name": "source", "label": "Source", "type": "text", "default_value": "voice-widget" },
{ "name": "rating", "label": "Rating", "type": "select", "options": ["1","2","3","4","5"], "default_value": "5" },
{ "name": "comment", "label": "Comment", "type": "textarea" }
]
}
]
}
}'Use for hidden/system fields the user shouldn’t have to think about.
Method 2 — Session metadata + system prompt
Pass user context at session start, and instruct the agent to use it.
Step 1 — Pass metadata when minting the token
// Your backend
const session = await fetch("https://api.oshara.ai/api/agents/agent-session/", {
method: "POST",
headers: { "Content-Type": "application/json", "Origin": "https://yoursite.com" },
body: JSON.stringify({
agent: "support-bot",
metadata: {
user_name: "Alice Smith",
user_email: "alice@acme.com",
user_company: "Acme Inc",
last_order_id: "ORD-77821"
}
})
}).then(r => r.json());Step 2 — Reference it in the system prompt
The current user is {{metadata.user_name}} ({{metadata.user_email}}) from
{{metadata.user_company}}. When you open a form, pre-fill any matching fields
(name, email, company) with these values without asking.Result
When the agent opens book-demo, it calls:
{
"name": "book_demo",
"arguments": {
"name": "Alice Smith",
"email": "alice@acme.com",
"company": "Acme Inc"
}
}The widget pre-fills those three fields. The user only fills in what’s left.
Method 3 — Agent tool arguments (conversation-derived)
Even without metadata, the agent extracts values from the conversation and passes them to the form tool:
User: “Hi, I’m Bob from Widgets Co — I’d love to see a demo.”
Agent → opens
book-demowith{ "name": "Bob", "company": "Widgets Co" }
This works for any field — the LLM matches user-mentioned values to field names. No code changes needed; just keep field names sensible (first_name, email, company, phone) so the LLM picks the right one.
Make it more reliable
Add explicit instructions in the system prompt:
When opening a form, look back at the conversation and pre-fill any field whose
value the user already mentioned. Field names: first_name, last_name, email,
company, phone, preferred_date.Method 4 — Direct publish from your app
If you have application state the agent doesn’t know about, publish a form.{id} message yourself. The widget treats it exactly like a message from the agent.
import { Room } from "livekit-client";
async function openFormWithValues(room, formId, prefill) {
const payload = JSON.stringify(prefill);
await room.localParticipant.publishData(
new TextEncoder().encode(payload),
{ topic: `form.${formId}` }
);
}
// Example — open the order-return form pre-filled with the last order
await openFormWithValues(room, "order-return", {
order_id: user.lastOrderId,
email: user.email,
reason: "",
});The widget opens the panel and pre-fills order_id and email. The agent will see the resulting form.state and continue verbally from there.
When to prefer Method 4 over 2/3
- You have data the agent doesn’t (a current cart, a row the user clicked on)
- You want the form to open at a specific UI moment (button click), not when the LLM decides
- You’re building a hybrid UI where forms aren’t always tied to the conversation
Pre-filling a multi-step form
Pre-fill values are matched by field name across all steps — you can fill fields on step 3 in the initial open call, and the user will see them populated when they reach that step:
{
"name": "book_demo",
"arguments": {
"first_name": "Alice", // step 1
"use_case": "Customer support", // step 2
"date": "2026-07-01" // step 3
}
}Putting it all together — recommended setup
// 1. Pass user context at session start
metadata: {
user_name: user.fullName,
user_email: user.email,
user_company: user.companyName,
account_tier: user.plan,
}
// 2. Pre-set hidden system fields with default_value
{ "name": "source", "default_value": "voice-widget" }
// 3. Let the agent merge conversation context via tool args automatically
// 4. For UI-driven opens (button click), publish form.{id} directly with extra app stateEffect: most fields arrive pre-filled. The agent only needs to ask about the truly unknown ones.
Verifying pre-fill is working
Watch the data channel during development:
room.on(RoomEvent.DataReceived, (payload, _p, _k, topic) => {
console.log(topic, JSON.parse(new TextDecoder().decode(payload)));
});You should see two messages per form open:
form.{id}from the agent with pre-fill valuesform.statefrom the widget reflecting those values + any user typing