Tutorial: custom buttons app
In this tutorial, you will build a Custom App that renders interactive buttons in DIAL Chat. The app demonstrates all button types — populate, action, and checkbox — in a multi-turn conversation flow. You will learn how to define buttons with JSON Schema, return them in responses, and handle user selections. Familiarity with the Getting started tutorial is assumed.
Prerequisites
- Completed the Getting started with DIAL SDK tutorial
- Docker and Docker Compose (V2) installed
What you will build
A button showcase application that guides the user through a multi-turn flow:
- Shows populate buttons as conversation starters (text inserted into the input box).
- Responds to the user's message, then shows action buttons with a confirmation dialog.
- Acknowledges the action, then shows checkbox buttons for multi-select.
- Summarizes all selections.
User → DIAL Chat → DIAL Core → Buttons App → DIAL Core → DIAL Chat → User
↕
(renders buttons via JSON Schema)
Step 1: Create the project directory
mkdir -p dial-buttons-app/buttons-app
mkdir -p dial-buttons-app/core
cd dial-buttons-app
Your project will have this layout when complete:
dial-buttons-app/
├── docker-compose.yml
├── core/
│ └── config.json
└── buttons-app/
├── app.py
├── requirements.txt
└── Dockerfile
Step 2: Implement the buttons app
Create the file buttons-app/app.py:
import uvicorn
from aidial_sdk import DIALApp
from aidial_sdk.chat_completion import ChatCompletion, Request, Response
POPULATE_SCHEMA = {
"type": "object",
"properties": {
"starter": {
"description": "Conversation starters",
"type": "number",
"dial:widget": "buttons",
"oneOf": [
{
"const": 1,
"title": "Tell me a joke",
"dial:widgetOptions": {
"populateText": "Tell me a funny joke about programming"
},
},
{
"const": 2,
"title": "Fun fact",
"dial:widgetOptions": {
"populateText": "Tell me a fun fact about AI"
},
},
{
"const": 3,
"title": "Help me write",
"dial:widgetOptions": {
"populateText": "Help me write an email to my team"
},
},
],
}
},
}
ACTION_SCHEMA = {
"type": "object",
"dial:chatMessageInputDisabled": True,
"properties": {
"decision": {
"description": "Do you want to continue?",
"type": "number",
"dial:widget": "buttons",
"oneOf": [
{
"const": 1,
"title": "Approve",
"dial:widgetOptions": {
"confirmationMessage": (
"Are you sure you want to approve?"
),
"submit": True,
},
},
{
"const": 2,
"title": "Reject",
"dial:widgetOptions": {
"confirmationMessage": (
"Are you sure you want to reject?"
),
"submit": True,
},
},
],
}
},
}
CHECKBOX_SCHEMA = {
"type": "object",
"definitions": {
"featuresEnum": {
"enumNames": [
"Streaming responses",
"File attachments",
"Custom buttons",
"Multi-turn memory",
],
"enum": ["streaming", "attachments", "buttons", "memory"],
}
},
"properties": {
"features": {
"description": "Select the features you want to enable",
"title": "Features",
"type": "array",
"uniqueItems": True,
"items": {"$ref": "#/definitions/featuresEnum"},
}
},
}
class ButtonsApplication(ChatCompletion):
async def chat_completion(
self, request: Request, response: Response
) -> None:
last_message = request.messages[-1]
form_value = None
if last_message.custom_content:
form_value = last_message.custom_content.form_value
if form_value and "features" in form_value:
selected = form_value["features"]
feature_list = ", ".join(selected) if selected else "none"
with response.create_single_choice() as choice:
choice.append_content(
f"You selected these features: {feature_list}.\n\n"
f"That completes the button showcase. You saw populate "
f"buttons, action buttons with confirmation, and checkbox "
f"buttons in action."
)
return
if form_value and "decision" in form_value:
decision = "approved" if form_value["decision"] == 1 else "rejected"
with response.create_single_choice() as choice:
choice.append_content(
f"You {decision} the action.\n\n"
f"Now select which features interest you:"
)
choice.set_form_schema(CHECKBOX_SCHEMA)
return
if form_value and "starter" in form_value:
user_text = last_message.content or "your request"
with response.create_single_choice() as choice:
choice.append_content(
f'You asked: "{user_text}"\n\n'
f"I would normally process that request here. "
f"For this demo, please approve or reject to continue:"
)
choice.set_form_schema(ACTION_SCHEMA)
return
with response.create_single_choice() as choice:
choice.append_content(
"Welcome to the button showcase app. "
"Choose a conversation starter below, or type your own message:"
)
choice.set_form_schema(POPULATE_SCHEMA)
app = DIALApp()
app.add_chat_completion("buttons-app", ButtonsApplication())
To run locally with uvicorn:
uvicorn app:app --host 0.0.0.0 --port 5000
Key concepts in this code:
choice.set_form_schema(schema)attaches a JSON Schema to the response. DIAL Chat renders it as interactive buttons.custom_content.form_valuecontains the user's button selections as a dictionary. Each key matches a property name from the schema.dial:widget: "buttons"tells DIAL Chat to render the JSON Schema property as buttons instead of a form field.dial:widgetOptionsconfigures button behavior:populateTextinserts text into the input,confirmationMessageshows a dialog,submit: truesends the form immediately.dial:chatMessageInputDisabled: truehides the text input box, forcing the user to interact only through buttons.
Step 3: Add dependencies
Create the file buttons-app/requirements.txt:
aidial-sdk==0.27.0
uvicorn==0.34.0
Step 4: Create the Dockerfile
Create the file buttons-app/Dockerfile:
FROM python:3.11-alpine
WORKDIR /app
COPY . /app
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 5000
CMD ["python", "app.py"]
Step 5: Configure DIAL Core
Create the file core/config.json:
{
"applications": {
"buttons-app": {
"displayName": "Button Showcase",
"description": "Demonstrates all DIAL button types in a multi-turn flow",
"endpoint": "http://buttons-app:5000/openai/deployments/buttons-app/chat/completions"
}
},
"keys": {
"dial_api_key": {
"project": "TEST-PROJECT",
"role": "default"
}
},
"roles": {
"default": {
"limits": {
"buttons-app": {}
}
}
}
}
Step 6: Write the Docker Compose file
Create docker-compose.yml in the project root:
services:
buttons-app:
build: buttons-app
depends_on:
- core
# ========= DIAL =========
core:
image: epam/ai-dial-core:0.25.1
platform: linux/amd64
environment:
aidial.config.files: '["/opt/config/config.json"]'
aidial.storage.overrides: '{ "jclouds.filesystem.basedir": "data" }'
aidial.redis.singleServerConfig.address: "redis://redis:6379"
aidial.identityProviders.test.rolePath: 'roles'
aidial.identityProviders.test.disableJwtVerification: 'true'
aidial.encryption.secret: 'salt'
aidial.encryption.key: 'password'
ports:
- "8080:8080"
volumes:
- ./core:/opt/config
depends_on:
- redis
themes:
image: epam/ai-dial-chat-themes:0.10.0
platform: linux/amd64
chat:
image: epam/ai-dial-chat:0.26.0
platform: linux/amd64
ports:
- "3000:3000"
depends_on:
- themes
- core
environment:
NEXTAUTH_SECRET: "secret"
THEMES_CONFIG_HOST: "http://themes:8080"
DIAL_API_HOST: "http://core:8080"
DIAL_API_KEY: "dial_api_key"
ENABLED_FEATURES: "conversations-section,prompts-section,top-settings,top-clear-conversation,top-chat-info,top-chat-model-settings,empty-chat-settings,header,footer,request-api-key,report-an-issue,likes,conversations-sharing,input-files,attachments-manager,custom-applications,marketplace"
redis:
image: redis:7.2.4-alpine3.19
command: >
redis-server
--maxmemory 256mb
--maxmemory-policy volatile-lfu
--save ""
--appendonly no
Step 7: Start the stack
docker compose up --build
Wait until DIAL Chat logs ready and DIAL Core logs Server started.
Verify: Open http://localhost:3000 in your browser. You should see the DIAL Chat interface.
Step 8: Test in DIAL Chat
- Open the Catalog and select Button Showcase.
- The app responds with a welcome message and three populate buttons: "Tell me a joke", "Fun fact", "Help me write".
- Click Tell me a joke. The text "Tell me a funny joke about programming" appears in the input box. You can edit it before sending.
- Send the message. The app acknowledges your request and shows two action buttons: Approve and Reject. The text input is disabled — you can only use buttons.
- Click Approve. A confirmation dialog appears asking "Are you sure you want to approve?". Confirm it.
- The app acknowledges the approval and shows checkbox buttons: "Streaming responses", "File attachments", "Custom buttons", "Multi-turn memory".
- Select one or more checkboxes, then click the send button.
- The app responds with a summary of your selected features.
Verify: You successfully interacted with all three button types — populate, action, and checkbox — in a single conversation.
Step 9: Test via the API
Send a request to get the initial response with buttons:
curl http://localhost:8080/openai/deployments/buttons-app/chat/completions \
-H "Content-Type: application/json" \
-H "api-key: dial_api_key" \
-d '{
"messages": [
{"role": "user", "content": "Hello"}
]
}'
Verify: The response includes custom_content.form_schema with the populate buttons schema.
Send a follow-up with a button selection in form_value:
curl http://localhost:8080/openai/deployments/buttons-app/chat/completions \
-H "Content-Type: application/json" \
-H "api-key: dial_api_key" \
-d '{
"messages": [
{"role": "user", "content": "Hello"},
{
"role": "assistant",
"content": "Welcome to the button showcase app. Choose a conversation starter below, or type your own message:"
},
{
"role": "user",
"content": "Tell me a funny joke about programming",
"custom_content": {
"form_value": {"starter": 1}
}
}
]
}'
Verify: The response acknowledges the selection and includes custom_content.form_schema with the action buttons schema.
Step 10: Stop the stack
Press Ctrl+C in the terminal running Docker Compose, then clean up:
docker compose down
Button types reference
| Type | Widget | Behavior | Use case |
|---|---|---|---|
| Populate | dial:widget: "buttons" with populateText | Inserts text into the chat input box | Suggested prompts, quick replies |
| Action | dial:widget: "buttons" with submit: true | Submits immediately, optional confirmation dialog | Approvals, binary decisions |
| Checkbox | type: "array" with uniqueItems: true | Multi-select from a list | Preferences, feature selection |
Widget options
Options available in dial:widgetOptions for each button entry:
| Option | Type | Effect |
|---|---|---|
populateText | string | Text inserted into the chat input box when pressed |
confirmationMessage | string | Text shown in a confirmation dialog before submission |
submit | boolean | When true, the form submits immediately after the button press |
Top-level schema options
| Option | Type | Effect |
|---|---|---|
dial:chatMessageInputDisabled | boolean | Disables free-text input — user can only interact via buttons |
required | array | Property names that must be filled before the user can submit |
What you learned
- How to define buttons using JSON Schema with
dial:widget: "buttons"anddial:widgetOptions. - How to attach a button schema to a response using
choice.set_form_schema(). - How to read user selections from
custom_content.form_valueon incoming messages. - The three button interaction patterns: populate (suggest text), action (submit with confirmation), and checkbox (multi-select).
- How to disable free-text input with
dial:chatMessageInputDisabledto force button-only interaction.
Further reading
- Getting started with DIAL SDK — build and run a first Custom App with DIAL SDK
- Tutorial: build a RAG app — build an app with file attachments and streaming responses
- Deploy a Custom App — take your buttons app to production with Helm
- Examples — explore more Custom App examples including button demos