Dialogs & App Home
Chattice supports two interaction-driven UI surfaces that go beyond plain message cards: dialogs (modal forms opened from a button) and the App Home tab (the bot's private space with a home card).
Both are synchronous-response flows: the handler returns a typed facade and the FastAPI integration serializes it into the documented REST response — no asynchronous calls, no manual JSON.
Opening a dialog: OPEN_DIALOG buttons
A button opens a dialog when its action carries interaction: OPEN_DIALOG
(the only documented value):
from chattice.cards import (
Button,
ButtonInteraction,
ButtonList,
Card,
CardHeader,
Section,
TextInput,
TextParagraph,
)
card = Card(
header=CardHeader(title="Contact"),
sections=[
Section(
widgets=[
TextParagraph("Add a contact"),
TextInput(name="name", label="Name"),
ButtonList(
buttons=[
Button(
"Open form",
action="open.contact",
interaction=ButtonInteraction.OPEN_DIALOG,
)
]
),
]
)
],
)
Clicking the button produces a CARD_CLICKED interaction with
isDialogEvent: true and dialogEventType: REQUEST_DIALOG — the same
envelope family as any other card click, so it routes through
@router.action("open.contact").
Dialog response
The handler for a REQUEST_DIALOG returns a Dialog facade wrapping a
Card body. The integration replies with
actionResponse.type=DIALOG and the dialog body under dialogAction.dialog:
from chattice.cards import Dialog, ActionStatus
from chattice.events import ActionEvent
from chattice import Router
router = Router()
@router.action("open.contact")
async def open_dialog(action: ActionEvent) -> Dialog:
return Dialog(
body=Card(sections=[Section(widgets=[TextInput(name="name", label="Name")])])
)
Response JSON:
{
"actionResponse": {
"type": "DIALOG",
"dialogAction": {"dialog": {"body": {"sections": [{"widgets": [{"textInput": {"name": "name", "label": "Name"}}]}]}}}
}
}
Submitting and cancelling: dialog observers
Submitting the dialog form (or cancelling it) produces a CARD_CLICKED
interaction with dialogEventType: SUBMIT_DIALOG / CANCEL_DIALOG. The
dispatcher routes these to dedicated observers:
@router.dialog_submit()— receives anActionEventwith the submitted values parsed intoevent.form_inputs(typedFormInputsmapping:StringInput,DateInput,DateTimeInput,TimeInput);@router.dialog_cancel()— receives the sameActionEventshape; returningNoneyields an empty 200 (the dialog closes silently).
actionStatus: OK and INVALID_ARGUMENT
A submit handler returns an ActionStatus facade — the only two documented
codes are OK and INVALID_ARGUMENT:
from chattice.events import StringInput
@router.dialog_submit()
async def submit(event: ActionEvent) -> ActionStatus:
name = event.form_inputs.get("name")
if not isinstance(name, StringInput) or not name.values:
return ActionStatus.invalid("Name is required")
return ActionStatus.ok("Saved")
ActionStatus.ok(message)→{"statusCode": "OK", "userFacingMessage": ...}(message optional) — the dialog closes.ActionStatus.invalid(message)→{"statusCode": "INVALID_ARGUMENT", "userFacingMessage": ...}— the dialog stays open showing the message.
Both serialize under actionResponse.dialogAction.actionStatus.
App Home: pushCard and updateCard
The App Home tab is served through the wrapped envelope family: the
interaction body nests under "chat" and the common data under
"commonEventObject" instead of the direct "type"/"common" keys (the
envelope normalizer accepts both).
APP_HOME(user opens the bot's Home tab) →@router.app_home()returns aCard; the integration replies with a RenderActions responseaction.navigations[].pushCard.SUBMIT_FORM(a form on the home card is submitted) →@router.form_submit()returns aCard; the integration replies withrenderActions.action.navigations[].updateCard.
@router.app_home()
async def home(event: AppHomeEvent) -> Card:
return Card(
header=CardHeader(title="Home"),
sections=[Section(widgets=[TextParagraph("Welcome")])],
)
@router.form_submit()
async def update(event: FormSubmitEvent) -> Card:
return Card(
header=CardHeader(title="Home"),
sections=[Section(widgets=[TextParagraph("Welcome")])],
)
Response JSON for APP_HOME:
{
"action": {
"navigations": [
{"pushCard": {"header": {"title": "Home"}, "sections": [{"widgets": [{"textParagraph": {"text": "Welcome"}}]}]}}
]
}
}
Restrictions
These surfaces are constrained by the Google Chat platform; the framework serializes what the docs allow, so keep the platform rules in mind:
- Dialogs are interaction-only. A
Dialogresponse is valid for a command or aREQUEST_DIALOGaction.SUBMIT_DIALOGacceptsActionStatus;CANCEL_DIALOGnormally returnsNone. Submit/cancel events cannot open a new Dialog. Invalid typed responses are rejected with a 500. - Dialog visibility is opener-only. A dialog is shown only to the user who triggered it; there is no way to send a dialog to someone else.
UPDATE_MESSAGEis bot-only. Replacing a card viaUPDATE_MESSAGEis only permitted when the message sender type isBOT. Updating cards on human messages usesUPDATE_USER_MESSAGE_CARDS(implemented, sender-derived) — see cards.- App Home is configured separately. Enable App Home in the Google Chat
API configuration in Google Cloud. The framework only serves the configured
HTTP endpoint once traffic reaches it. App Home interactions
carry a private DM
spaceand are only sent to the individual user.