How to add a button to your chat widget's home screen, turn it into a form, choose where the answers go, and check they arrived. Written for support staff and customers, with screenshots from a real widget.
Answers are always saved to Assistable first. An integration is an optional extra destination, never a requirement.
Every submitted form is written to Assistable's own records and, whenever there is something a human would want to read, it also opens a conversation in your Inbox. That happens before anything is forwarded anywhere, and it happens whether or not a tool is chosen, whether or not Gleap, Zendesk, GHL or Stripe is connected, and even when the forward fails outright.
If Gleap or Zendesk is connected, a ticket goes there too. If neither is connected, the ticket lives in the Assistable inbox and your team works it there. Nothing is ever lost because an integration is missing or broken.
This matters because the most common worry when setting up a form is "what if I haven't connected our helpdesk yet?" The answer is: set the form up anyway. It will work. Connect the helpdesk later and new submissions start flowing there as well, with no change to the form.
Home actions are the buttons on the widget's Home screen, the first thing a visitor sees when they open the chat bubble. Each one is a card with a title, an optional subtitle, and an icon.
A typical support widget shows something like:
You configure them in the dashboard under Portal › Widgets › (your widget) › Tabs & content, in the Home action cards section. Widget templates (General purpose, Support help desk) pre-fill a sensible set, and you can edit, reorder or delete them.
Every action has a mode that decides what happens when a visitor taps it.
| Mode | What happens | When to use it |
|---|---|---|
chat |
Opens the chat. If you set a starter message it is sent to the AI immediately; otherwise the card's title is sent as the visitor's first message. | Anything conversational: questions, sales, troubleshooting with the AI. |
form |
Opens a form built from the fields you define. On submit the answers are saved, and optionally forwarded to one destination. | Structured input: bug reports, feature requests, NPS surveys, lead capture. |
link |
Opens a URL in a new browser tab. | Status pages, docs, booking pages, anything external. |
A form action with zero fields cannot be saved, and a
link action with no URL cannot be saved either: the editor
blocks the whole save and shows an error. A chat card with no
starter message sends its own title as the message, so name those cards so
they read naturally as a sentence ("Ask a question", not "Card 3").
One continuous recording: adding a support-ticket form in the dashboard, setting its title, subtitle and icon, choosing a destination, adding fields, saving, and then filling the form in as a visitor on a live site.
Go to Portal › Widgets. Every widget in the subaccount is listed with the assistant it is linked to.
The editor opens on the Assistant section, with a live preview of the widget on the right. That preview updates as you edit, so you can see a new card appear before you save.
Switch to Tabs & content and open the Home card. Under the header and sub-header fields you will find Home action cards, listing the buttons this widget shows today. Each row shows its mode and, for forms, the destination and field count.
form · create_support_ticket · 5 fields.
Scroll to Add from template. The presets are complete, working actions, labelled Bug report, Feature request, NPS survey, Order status (commerce) and Ask a question (chat). Starting from a preset is easier than starting blank because the field keys are already filled in correctly.
Here we click Bug report, which creates a form wired to a support ticket.
Click the pencil on the new card to expand it. The Title is the bold line the visitor reads, the Subtitle is the smaller line under it, and the Icon sits on the right of the card.
The icon field is free text and expects a lucide icon
name in kebab-case, for example help-circle,
alert-triangle, smile, bug,
package, credit-card, calendar,
star, zap. Case and punctuation are forgiven, so
HelpCircle also works, but a name outside the supported set
silently falls back to the generic message bubble. That fallback is the
usual reason every card ends up with the same icon.
bug icon set on the new card.The What should happen with this form? dropdown picks the single optional destination for a copy of the answers. Expanded, the full list is:
"Just collect the answers" is a perfectly good choice. Everything still lands in your Inbox. Pick a destination only when you genuinely need the answers in another system. See the destinations table for what each one needs.
"Create support ticket (Zendesk)" does not require Zendesk. It routes to whichever helpdesk is connected and falls back to the Assistable inbox when none is. Likewise "Submit feedback (Gleap)" saves feedback locally first and only mirrors to Gleap if Gleap happens to be connected.
Each field row has four controls: the label the visitor
reads, the type, a Required toggle, and
the key box on the right (this is maps_to).
Here we added Your name and Your email to the preset's subject, description and priority, so the ticket has someone to reply to. The email field's type is set to Email so the widget validates the address, and both are marked Required.
Saved as "...") until you type an override.
Field types available:
| Type | What the visitor sees |
|---|---|
| Short text | One-line text input |
| Long text | Multi-line text box |
| Email input; the format is checked before submit | |
| Dropdown | A select built from comma-separated options you supply |
| Single choice (radio) | A radio list built from your options |
| Checkbox (yes/no) | One tick box |
| Smiley rating (1-5) | Five faces, stored as 1 to 5 |
| NPS (0-10) | A 0 to 10 row labelled "Not at all likely" / "Extremely likely" |
| Numeric scale | A numeric row with a min and max you set (defaults 1 to 5) |
| Priority (low/normal/high/urgent) | Four buttons: Low, Normal, High, Urgent |
Required is enforced twice: the widget will not submit while a required field is empty (a required checkbox must be ticked), and the server checks again on arrival.
Click Save changes. Saving validates every action (a form needs at least one field, a link needs a URL) and pushes the change live. A success toast confirms it.
Embedded widgets pick the change up within roughly a minute: the configuration is cached briefly in the browser and on the server. If your change does not appear straight away, wait a minute and hard-refresh the page before assuming the save failed.
These are captured from the widget running on an ordinary web page, loading the configuration we just saved.
Read this table with the guarantee from section 1 in mind: whatever the "if it is not connected" column says, the answers are already saved on Assistable's side before the forward is even attempted.
| Destination | What it does | What must be connected | If it is NOT connected |
|---|---|---|---|
| Just collect the answers | Saves the answers and opens a conversation in your Inbox. | Nothing. | Nothing to break. |
create_ticketcreate_support_ticket |
Creates a support ticket and routes it. Both names behave identically. Routing order is Zendesk, then Gleap, then the Assistable inbox; the first connected one wins. | Nothing for the in-app route. Zendesk or Gleap only if you want the ticket to appear there. | Falls down the chain to the Assistable inbox, which always works. A
Zendesk failure falls back silently to Gleap and then in-app,
so a ticket may exist somewhere other than where you expected. Check
the ticket's destination before concluding it vanished. |
submit_feedback |
Saves a feedback record (kind, rating, scale, comment). If Gleap is connected it is mirrored there in the background. | Nothing. Gleap optional. | The feedback is still saved. A failed Gleap mirror is recorded against the record and logged; the visitor never sees an error. |
ghl_contact |
Creates or updates a GHL contact from the email, phone and name fields, adds the configured tag, and attaches any remaining answers as a contact note. | A GHL connection on the subaccount, at least one field mapped to
email or phone, and the tag your GHL
workflow triggers on. |
Fails with ghl_not_connected, or
email_or_phone_required if nothing mapped to email or
phone. If the contact is created but the tag fails you get
ghl_tag_failed: the contact exists but your automation
never fired. Answers are saved either way. |
webhook |
POSTs the mapped answers to the URL you put on the action, as
{source, subAccountId, submittedAt, fields}. |
A public URL on port 80 or 443 that returns 2xx within 8 seconds and
does not redirect. http:// is accepted as well as
https://. Private and internal addresses are blocked. |
One attempt, no retries. The failure is logged and reported as not delivered; the answers are still saved. For guaranteed delivery use a workspace webhook subscription instead. |
search_kb |
Searches the knowledge bases attached to the widget's assistant. A lookup: nothing is stored. | An assistant with at least one knowledge base. | Returns nothing. Caveat: the form screen does not
currently display lookup results, so use a chat card for
knowledge-base questions instead. |
get_subscription_status |
Looks up the Stripe subscription for a given email. A lookup. | A Stripe integration on the subaccount, and a field mapped to
email. |
Fails with stripe_not_connected or
subscription_not_found and the visitor sees a generic
error. Same display caveat as above. |
list_invoices |
Lists Stripe invoices for a given email. A lookup. | As above. | As above. |
When a form is submitted the server reports two things on purpose:
In the JSON that comes back from actions/submit these are the
ok and delivered fields, with
delivery_error alongside them when a forward failed.
ok) means the answers are kept. This
is what decides whether the visitor sees the success screen.delivered) means the forward you
asked for actually happened. When it is false,
delivery_error carries the reason.They are separate so a dead destination, an expired Zendesk token or a webhook receiver that is down, never makes a visitor retype answers you already have. The flip side is that a failed forward is invisible to the visitor: they see success either way. When someone reports "the form worked but nothing arrived", the delivered flag in the submit response is the first thing to check.
The three lookups are the exception. They store nothing, so when they fail they simply fail, and the visitor sees an error.
maps_to
Every field gets a random internal id such as f-a3k9 when you
add it, and the raw answers are always stored under those ids. But when the
answers are forwarded to a destination, each answer needs a
meaningful key: a ticket needs to know which answer is the subject,
GHL needs to know which is the email address. That key is what the
maps_to box sets.
maps_to, that is used.email, phone, first_name,
last_name or name, that is used. So a field
labelled simply "Email" needs no maps_to at all.email.contact. and ticket. prefixes
A leading contact. or ticket. is accepted and
stripped automatically. contact.email becomes
email, ticket.subject becomes
subject, ticket.body becomes body.
The stock templates ship keys in that prefixed form, so both styles are
valid and mean the same thing. Use whichever reads better to you.
Rule of thumb: set maps_to explicitly for any field a
destination needs. The editor shows the inferred key as the
placeholder text in the box, so you can see at a glance whether inference is
going to do the right thing. You must set it yourself whenever the label does
not slugify to what the destination expects: a field labelled "Work email
address" needs email, and a ticket subject field labelled
"What's this about?" needs subject.
| Destination | Keys it reads |
|---|---|
| Support ticket | subject, description (or body),
priority; requester email from email,
requester_email or visitor_email;
requester name from name, requester_name,
visitor_name or first_name |
| Feedback | message, description, comment
or title for the comment; type,
severity; plus the email and name keys above. The score
comes from the rating, NPS or scale field automatically. |
| GHL contact | email, phone, first_name,
last_name, name, tag.
Anything else is attached to the contact as a note. |
| Stripe lookups | email |
| Knowledge-base search | query |
The submission captured in section 6 was a support-ticket form on a widget with no Zendesk and no active Gleap integration, which is the most common starting position. Here is exactly what it produced, read back from the database.
Written for every non-lookup submission. This is the "always saved" part of the guarantee.
SELECT _id, "actionId", "actionTitle", tool, "contactName", "contactEmail",
"contactId", "conversationId", "visitorId", "createdAt"
FROM "widgetFormSubmission"
WHERE "widgetId" = 'cmqj7kubf0001rua7841r0345'
ORDER BY "createdAt" DESC LIMIT 1;
-[ RECORD 1 ]--+---------------------------------------------
_id | cmspi9htb000be0ruk4iaj81z
actionId | bug-vsh3mt
actionTitle | Report a bug
tool | create_support_ticket
contactName | Dana Whitfield
contactEmail | dana.whitfield@northwind-demo.test
contactId | cmspi54l00000e0rutazat78n
conversationId | cmspi9ht40009e0ruxneacx95
visitorId | visitor_7a3e5fc1-915c-4026-872c-b0318561bd6a
createdAt | 2026-08-12 03:04:05.999
The raw answers live on the same row, keyed by field id:
SELECT jsonb_pretty(values) FROM "widgetFormSubmission"
WHERE _id = 'cmspi9htb000be0ruk4iaj81z';
{
"f-19tc": "Dana Whitfield",
"f-a36o": "dana.whitfield@northwind-demo.test",
"subject": "Checkout fails on the final step",
"priority": "high",
"description": "Adding a cordless drill to the basket works, but pressing
Pay returns an error: 'order could not be created'.
Happens in Chrome and Safari, reproduced three times
this morning."
}
maps_to is for
Look at those keys. subject, priority and
description are readable because they came from a preset,
which gave them meaningful ids. The two fields we added by hand got random
ids, f-19tc and f-a36o, because that is simply how
new fields are created.
Those random ids are not a problem here, because we set
maps_to on both. The stored copy keeps the raw ids, but the
ticket below still knows which answer was the name and which was the email.
Had we left maps_to blank on a field labelled something the
system cannot guess, that answer would have reached the ticket as
f-19tc and been ignored.
Created because the action's destination was a support ticket. Subject,
priority and requester are all populated from the mapped fields, and
destination records where it was routed.
SELECT _id, subject, priority, status, source, "requesterName",
"requesterEmail", destination, "externalId", "externalUrl",
"contactId", "conversationId", "submissionId", "createdAt"
FROM "supportTicket"
WHERE "widgetId" = 'cmqj7kubf0001rua7841r0345'
ORDER BY "createdAt" DESC LIMIT 1;
-[ RECORD 1 ]--+-----------------------------------
_id | cmspi9htq000ce0ruzahypobs
subject | Checkout fails on the final step
priority | high
status | open
source | home-action
requesterName | Dana Whitfield
requesterEmail | dana.whitfield@northwind-demo.test
destination | in-app
externalId |
externalUrl |
contactId | cmspi54l00000e0rutazat78n
conversationId | cmspi9ht40009e0ruxneacx95
submissionId | cmspi9htb000be0ruk4iaj81z
createdAt | 2026-08-12 03:04:06.014
destination = in-app is the fallback working exactly as designed:
no helpdesk was connected, so the ticket lives in Assistable and the team
works it from the Inbox. Had Zendesk or Gleap been connected, this would read
zendesk or gleap and the row would also carry the
external ticket id and URL.
The name and email fields were mapped to contact.name and
contact.email, so the visitor was matched to (or created as) a
contact, and the submission and ticket are both linked to it.
SELECT _id, "firstName", "lastName", name, email, phone,
"subAccountId", "createdAt"
FROM contact
WHERE email = 'dana.whitfield@northwind-demo.test';
-[ RECORD 1 ]+-----------------------------------
_id | cmspi54l00000e0rutazat78n
firstName | Dana
lastName | Whitfield
name | Dana Whitfield
email | dana.whitfield@northwind-demo.test
phone |
subAccountId | 1753298396627x629167684234041700
createdAt | 2026-08-12 03:00:42.228
Note that contact.name was split into firstName and
lastName automatically, and the same contact id appears on both
the submission and the ticket above, so the visitor's history stays joined up.
The submission also opened a conversation containing the answers as a readable message, retitled with the ticket subject. That conversation is what your team actually replies to when no helpdesk is connected.
SELECT c._id, c."lastChannel", c."lastSubjectLine", c."contactEmail",
c."previewText"
FROM conversation c
JOIN "widgetFormSubmission" s ON s."conversationId" = c._id
WHERE s._id = 'cmspi9htb000be0ruk4iaj81z';
-[ RECORD 1 ]---+---------------------------------------------------
_id | cmspi9ht40009e0ruxneacx95
lastChannel | Live_Chat
lastSubjectLine | Checkout fails on the final step
contactEmail | dana.whitfield@northwind-demo.test
previewText | Adding a cordless drill to the basket works, but
| pressing Pay returns an error: 'order could not be
| created'. Happens in Chrome and Safari, reproduced
| three times this morning.
One exception: a submission that is nothing but a rating, for example
a bare NPS score with no comment, does not open a conversation. Those are
stored in the submission and feedback records only. Ticket destinations are
exempt from the exception: create_ticket and
create_support_ticket always open a conversation, even from a
bare rating. If you want NPS responses to be visible to your team, add an
optional comment field to the survey.
First diagnostic step for any form problem: open browser dev tools, go to the
Network tab, submit the form, and read the JSON response of the
actions/submit request. The widget deliberately shows only a
generic message; the response body carries the real reason.
| Symptom | Likely cause | What to check and fix |
|---|---|---|
| The form shows "Sorry, that didn't go through" | The server rejected the submission. The generic message hides the real error. | Read the response body.
Missing required field: X means a required field was
empty, or the widget is running a stale config.
tool_not_allowed means the destination string is not one
of the supported values, usually a typo made in the code editor.
action_not_form_mode or action_not_found
means the widget is running a cached config or the action was deleted:
wait a minute, or re-save the widget.
HTTP 403 means the embedding domain is not on the widget's allowed
domains list. HTTP 429 is the rate limit. |
| Answers are saved but nothing reached GHL | The local save succeeded and the forward failed afterwards. This is the "saved but not delivered" case. | Read the delivery error code.
ghl_not_connected: the subaccount has no GHL location or
token. email_or_phone_required: no submitted field mapped
to email or phone; the editor shows a red
warning for this. ghl_tag_failed: the contact was created
but the tag could not be added, so any workflow triggered by that tag
never fired; check the tag exists in GHL. Also confirm the visitor
actually filled the email or phone field, because empty answers are
dropped before mapping. |
| No webhook fired | Two different features are both called "webhook", or the endpoint is not subscribed. | Decide which one you mean.
The per-form webhook destination needs a URL set on the
action; it must be a public http:// or
https:// address on port 80 or 443, answer 2xx within 8
seconds and not redirect. It gets one attempt and no retries; check
the delivery error (webhook_url_required,
webhook_blocked: …, webhook_http_500,
webhook_delivery_failed).
Workspace webhook subscriptions are separate: the endpoint
must exist under Dashboard › Integrations › Webhooks, be
active, and have widget.form.submitted ticked. Those are
signed and retried — four attempts in total, so three retries,
spaced roughly 1, 3 and 6 hours apart — and every attempt is
logged. Note that lookups never emit the event, and the subaccount
must belong to a workspace. |
| The action is missing from the widget | Not saved, dropped as invalid, or cached. | Did the save actually succeed? A form with no fields or a link with no URL blocks the entire save with an error toast, so an unrelated broken card stops your new one from saving too. An action with no title is silently dropped when the config is served. Otherwise it is caching: the config is held for about 30 seconds in the browser and up to five minutes on the server, so wait a minute and hard-refresh. If it still does not appear, confirm the page embeds the right widget id and that the widget is not archived. |
| All the icons look identical | The icon names are not recognised, so every card falls back to the same generic message bubble. | The icon box is free text with no validation and no picker. Use a
supported lucide name in kebab-case: bug,
lightbulb, smile, help-circle,
alert-triangle, package,
credit-card, calendar, star,
zap. Case and punctuation are forgiven, but any name
outside the supported set falls back silently. Check the live preview
in the editor: if the icon does not change as you type, the name is
not recognised. |
| A ticket "went missing" or landed in the wrong place | Routing precedence and silent fallback. | The order is Zendesk, then Gleap, then the Assistable inbox, and a
Zendesk API failure falls back silently. Check the ticket's
destination value. Remember the dropdown label
"(Zendesk)" is misleading: the destination works with nothing
connected at all. |
| A second report from the same visitor never appeared | Deduplication. | There is one open ticket per conversation. A repeat request updates the existing ticket and appends the new text to the conversation rather than filing a duplicate, and no new notification is sent. A new ticket only opens once the previous one is resolved. |
| NPS or rating responses seem to vanish | A submission that is only a rating creates no Inbox conversation, unless its destination is a support ticket. | Bare ratings live in the submission and feedback records, which have
no dashboard screen yet. Add an optional comment field so submissions
create Inbox threads, or subscribe to the
widget.form.submitted event. |
| Edits saved in the dashboard do not show on the live site | Config caching, or a stale widget script on the page. | Wait about a minute and hard-refresh. If the feature rather than the configuration is missing, the site may be loading an old copy of the widget script; check the script source the page embeds. |