Skip to Content
DocumentationGuidesForm Rendering

How it works end-to-end

1. You define a form in the character's appearance config 2. Agent gains a tool named after the form's id 3. During the call, LLM decides to open the form (e.g. user says "I'd like to book a demo") 4. Widget opens the form panel with any pre-filled fields 5. User fills in the remaining fields (agent guides verbally) 6. User clicks Submit → widget POSTs to your submit_url 7. Agent receives confirmation and continues the conversation

Step 1 — Define the form

Add a forms array to your character’s widget_appearance. Each form needs an id, a title, the fields, and a submit_url.

Single-page form

PATCH /api/ai-characters/support-bot/ { "widget_appearance": { "forms": [ { "id": "book-demo", "title": "Book a demo", "submit_url": "https://yoursite.com/api/demo-requests", "submit_label": "Confirm booking", "success_message": "We'll be in touch within one business day!", "fields": [ { "name": "name", "label": "Your name", "type": "text", "required": true }, { "name": "email", "label": "Work email", "type": "email", "required": true }, { "name": "company", "label": "Company", "type": "text" }, { "name": "date", "label": "Preferred date","type": "date", "required": true } ] } ] } }

Multi-step form (stepper)

Use steps instead of fields when the form has multiple screens:

{ "id": "onboarding", "title": "Get started", "submit_url": "https://yoursite.com/api/onboarding", "steps": [ { "id": "details", "title": "Your details", "fields": [ { "name": "first_name", "label": "First name", "type": "text", "required": true, "width": "half" }, { "name": "last_name", "label": "Last name", "type": "text", "required": true, "width": "half" }, { "name": "email", "label": "Work email", "type": "email", "required": true } ] }, { "id": "use-case", "title": "Your use case", "fields": [ { "name": "use_case", "label": "What are you building?", "type": "select", "required": true, "options": ["Customer support", "Sales assistant", "Internal tool", "Other"] }, { "name": "team_size", "label": "Team size", "type": "number" } ] }, { "id": "schedule", "title": "Book a time", "fields": [ { "name": "date", "label": "Preferred date", "type": "date" }, { "name": "timezone", "label": "Your timezone", "type": "text" } ] } ] }

Step 2 — Receive submissions in your backend

The widget sends a POST request to your submit_url when the user submits the form. The body is a flat JSON object of field name → value.

Example payload for the book-demo form above:

{ "name": "Alice Smith", "email": "alice@acme.com", "company": "Acme Inc", "date": "2025-07-01" }

Express handler example:

app.post("/api/demo-requests", express.json(), async (req, res) => { const { name, email, company, date } = req.body; // 1. Save to your database await db.demoRequests.create({ name, email, company, scheduledDate: date }); // 2. Send internal notification await slack.postMessage({ channel: "#sales", text: `New demo request: ${name} (${email}) from ${company} — ${date}` }); // 3. Send confirmation email to the user await sendgrid.send({ to: email, subject: "Demo confirmed!", text: `Hi ${name}, your demo is booked for ${date}.` }); res.status(200).json({ ok: true }); // Any 2xx = success });

Return any 2xx status to tell the widget the submission succeeded. The widget shows success_message and the agent continues the conversation. On non-2xx, the widget shows an error and the agent is notified to try again.


Step 3 — Prompt the agent to use forms

Add an instruction in your character’s system prompt telling the agent when to open the form:

When the user expresses interest in a demo, scheduling a call, or speaking with the sales team — open the 'book-demo' form. Pre-fill any details you already know (name, email, company) from the conversation or from the session metadata. After the form is submitted, confirm the booking verbally and offer to answer any remaining questions.

The agent will call the book_demo tool (note: hyphens in the id become underscores in the tool name) when it decides the time is right.


Step 4 — Agent pre-filling fields

The agent can pre-fill form fields with values it already knows from:

  • The conversation (“My name is Alice”)
  • Session metadata you passed at session start (user_name, user_email, etc.)

When the agent opens the form, it passes known values as tool arguments:

{ "name": "book_demo", "arguments": { "name": "Alice", "email": "alice@acme.com" } }

The widget pre-fills those fields immediately. The user only needs to fill in the rest.

To pre-fill reliably, make sure your session start passes user context:

// Session start from your backend { "agent": "support-bot", "metadata": { "user_name": "Alice Smith", "user_email": "alice@acme.com", "user_company": "Acme Inc" } }

And reference it in the system prompt:

The user's name is {{metadata.user_name}}, email is {{metadata.user_email}}. Use this to pre-fill forms wherever possible.

Step 5 — Using managed storage instead of your own endpoint

If you don’t want to set up a webhook endpoint, use submit_url: null — Oshara stores the submissions for you.

{ "submit_url": null }

Retrieve them later via the API:

GET /api/agents/support-bot/form-responses/?form_id=book-demo Authorization: Bearer <your-token>
[ { "id": 1, "form_id": "book-demo", "session_id": "sess_abc123", "values": { "name": "Alice", "email": "alice@acme.com", "date": "2025-07-01" }, "created_at": "2025-06-10T15:00:00Z" } ]

Field types reference

TypeUse for
textNames, short answers
emailEmail addresses (auto-validated)
telPhone numbers
textareaLong text / notes
selectDropdown choice from a fixed list
numberQuantities, counts
dateDate picker
timeTime picker
checkboxSingle yes/no toggle
radioMutually exclusive options shown as buttons
displayRead-only text block (no input, no name)

Two-column layout

Add "layout": { "field_layout": "grid" } to the form and "width": "half" to individual fields to display them side by side:

{ "id": "contact", "layout": { "field_layout": "grid" }, "fields": [ { "name": "first_name", "label": "First name", "type": "text", "width": "half" }, { "name": "last_name", "label": "Last name", "type": "text", "width": "half" }, { "name": "email", "label": "Email", "type": "email", "width": "full" } ] }

Checklist

  • Form defined in widget_appearance.forms with a unique id
  • submit_url pointing at your backend endpoint (or null for managed storage)
  • Backend endpoint returns 2xx on success
  • System prompt instructs agent when to open the form
  • Session metadata passes user details for pre-fill
  • success_message set so user knows what happens next
Last updated on