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 conversationStep 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
metadatayou 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
| Type | Use for |
|---|---|
text | Names, short answers |
email | Email addresses (auto-validated) |
tel | Phone numbers |
textarea | Long text / notes |
select | Dropdown choice from a fixed list |
number | Quantities, counts |
date | Date picker |
time | Time picker |
checkbox | Single yes/no toggle |
radio | Mutually exclusive options shown as buttons |
display | Read-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.formswith a uniqueid -
submit_urlpointing at your backend endpoint (ornullfor managed storage) - Backend endpoint returns
2xxon success - System prompt instructs agent when to open the form
- Session
metadatapasses user details for pre-fill -
success_messageset so user knows what happens next