Assistable chat widget

Setting up home-action forms

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.

On this page

  1. The one thing to know first
  2. What home actions are
  3. The three modes: chat, form, link
  4. Video: the whole thing end to end
  5. Step by step: building a form
  6. What the visitor sees
  7. Destinations: where answers can be sent
  8. Field keys and maps_to
  9. Where the data actually landed
  10. Troubleshooting

1. The one thing to know first

The core guarantee

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.

2. What home actions are

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.

3. The three modes

Every action has a mode that decides what happens when a visitor taps it.

ModeWhat happensWhen 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.
Worth knowing

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").

4. Video: the whole thing end to end

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.

Configuring and then submitting a home-action form. Recorded against a local dashboard and widget at 1440×900.

5. Step by step: building a form

1 Open the widgets list

Go to Portal › Widgets. Every widget in the subaccount is listed with the assistant it is linked to.

The Widgets list in the Assistable portal, showing several widgets with their linked assistants.
The widgets list. Click a widget's name to open its editor.

2 Open the widget editor

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.

The widget editor open on the Assistant section, with a live preview of the widget on the right.
The editor sections run down the left: Assistant, Tabs & content, Appearance, Integrations & tools, Behavior, Install.

3 Find the Home action cards

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.

The Home action cards section of the widget editor listing four existing actions.
Four existing cards. Note the summary line under each title, for example form · create_support_ticket · 5 fields.

4 Add an action from a preset

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.

The Add from template section showing preset cards including Bug report, Feature request, NPS survey and Order status.
The preset gallery. "Add blank card" below it starts from nothing.

5 Set the title, subtitle and icon

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.

Icon names

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.

The expanded action card in the editor showing Title, Subtitle and Icon inputs filled in.
Title, subtitle and the bug icon set on the new card.

6 Choose what happens with the form

The What should happen with this form? dropdown picks the single optional destination for a copy of the answers. Expanded, the full list is:

The destination dropdown expanded, showing all eight options from Just collect the answers through to List invoices.
All destinations: just collect, GHL contact, webhook URL, support ticket, feedback, knowledge-base search, subscription check, invoice list.

"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.

Two labels are misleading

"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.

7 Build the form fields

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.

The form fields editor showing field labels, type dropdowns, Required toggles and the maps_to key boxes.
The field editor. The key box shows the inferred key as grey placeholder text (Saved as "...") until you type an override.

Field types available:

TypeWhat the visitor sees
Short textOne-line text input
Long textMulti-line text box
EmailEmail input; the format is checked before submit
DropdownA 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 scaleA 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.

8 Save

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.

The widget editor after saving, with a success toast reading Widget updated successfully.
The "Widget updated successfully" toast.

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.

6. What the visitor sees

These are captured from the widget running on an ordinary web page, loading the configuration we just saved.

A demo website with the chat widget open, showing five home action cards each with a distinct icon.
The widget's home screen. Each card carries its own icon, and the new "Report a bug" card has joined the four that were already there.
The widget showing the opened bug report form with empty Subject, What happened, Priority, Your name and Your email fields.
Tapping the card opens the form. Required fields are marked with a red asterisk.
The same form filled in with a subject, a description, High priority selected, a name and an email address.
Filled in, with High selected on the priority row.
The widget showing the success state after submitting, with a green tick, the word Submitted and a confirmation message.
The success state. The message shown is the action's "success message", which you can set in the editor.

7. Destinations: where answers can be sent

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_ticket
create_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.

Saved versus delivered are two separate facts

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.

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.

8. Field keys and 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.

How the key is chosen

  1. If you typed something in maps_to, that is used.
  2. Otherwise the label is slugified: lowercased, with non-alphanumeric characters turned into underscores. If the result is one of email, phone, first_name, last_name or name, that is used. So a field labelled simply "Email" needs no maps_to at all.
  3. Otherwise, if the field type is Email, the key is email.
  4. Otherwise the random field id is used, which no destination understands.
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.

Keys each destination looks for

DestinationKeys 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 lookupsemail
Knowledge-base searchquery

9. Where the data actually landed

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.

The submission record

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."
}
This is what 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.

The support ticket

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 contact

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.

And in the Inbox

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.

10. Troubleshooting

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.