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.
MethodWhen the value is setSource of truth
1. default_value on the fieldAt render time, before the agent does anythingHardcoded in form definition
2. Session metadata + system promptAgent fills from metadata when opening formYour database, passed at session start
3. Agent tool argumentsAgent fills from conversation contextWhat the user said + metadata
4. Direct data-channel publishYour code publishes a form.{id} message with valuesYour 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-demo with { "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
  }
}
// 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 state
Effect: 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:
  1. form.{id} from the agent with pre-fill values
  2. form.state from the widget reflecting those values + any user typing