Widget Button Pattern for ChatGPT Apps SDK
Widget Button Pattern for ChatGPT Apps SDK
Section titled “Widget Button Pattern for ChatGPT Apps SDK”Summary
Section titled “Summary”This document explains the correct pattern for implementing interactive buttons in ChatGPT widget HTML files that trigger MCP tools.
TL;DR: Use window.openai.sendFollowUpMessage() for ALL interactive buttons. The callTool() API is documented but NOT currently available in the ChatGPT Apps SDK (confirmed via console inspection showing typeof window.openai.callTool === 'undefined').
The Two API Methods
Section titled “The Two API Methods”The ChatGPT Apps SDK provides two different methods for triggering actions:
1. sendFollowUpMessage() - For ALL Interactive Buttons (Current Working Approach)
Section titled “1. sendFollowUpMessage() - For ALL Interactive Buttons (Current Working Approach)”IMPORTANT: As of now, callTool() is NOT available in the ChatGPT Apps SDK. Use sendFollowUpMessage() for all interactive buttons.
Use this for both initial screens AND widget buttons:
// ✅ Works from welcome.html (non-widget)await window.openai.sendFollowUpMessage({ prompt: "Show my profile" });
// ✅ Works from widget buttons (user-profile.html, shopping-list.html)await window.openai.sendFollowUpMessage({ prompt: "Create a shopping list for me" });When to use:
- Initial app screens (welcome pages, landing pages)
- Buttons inside widget HTML that are displayed as tool results
- When you want ChatGPT to interpret the request and invoke tools
- When you need conversational flow
2. callTool() - NOT YET AVAILABLE
Section titled “2. callTool() - NOT YET AVAILABLE”⚠️ This API is documented but NOT currently available in the ChatGPT Apps SDK.
The intended API for direct tool invocation from widgets would be:
// ❌ Does NOT work - callTool is undefinedconst result = await window.openai.callTool("create_shopping_list", {});Evidence:
- Console checks show
typeof window.openai.callTool === 'undefined' - Current production widget implementations use
sendFollowUpMessageinstead - Local test mocks only implement
sendFollowUpMessage, notcallTool
When it becomes available, use it for:
- Direct tool invocation without conversational interpretation
- Immediate, specific actions with no AI decision-making
- Buttons inside widget HTML that need direct server calls
The Working Pattern for Widget Buttons
Section titled “The Working Pattern for Widget Buttons”Current Reality: Since callTool() is not yet available, buttons in widget results use window.openai.sendFollowUpMessage() with conversational prompts.
Correct Implementation (Current Working Approach)
Section titled “Correct Implementation (Current Working Approach)”// Reusable AI action handler - uses sendFollowUpMessagefunction useAIAction(prompt, actionName = 'action') { const [isLoading, setIsLoading] = useState(false);
const handleAction = async () => { console.log(`🚀 ${actionName} button clicked`); setIsLoading(true); try { console.log(`📤 Sending follow-up message: ${prompt}`); await window.openai.sendFollowUpMessage({ prompt }); console.log('✅ Follow-up message sent successfully'); } catch (error) { console.error(`❌ ${actionName} error:`, error); setIsLoading(false); } // Don't set isLoading to false - let ChatGPT handle the transition };
return [isLoading, handleAction];}
// Usage in componentconst [isCreatingList, handleCreateList] = useAIAction("Create a shopping list for me", "Create Shopping List");Key Characteristics
Section titled “Key Characteristics”- Uses
sendFollowUpMessage()- Current working API for widget buttons - Conversational prompts - Sends natural language prompts that ChatGPT interprets
- Loading state management - Sets loading to
true, but does NOT set it tofalse(ChatGPT handles the transition) - Error handling - Only sets loading to
falseif there’s an error - Reusable hook pattern -
useAIAction()makes implementation consistent across widgets
Why This Works
Section titled “Why This Works”sendFollowUpMessage()sends a conversational prompt to ChatGPT- ChatGPT interprets - ChatGPT decides which tool to invoke based on the prompt
- Good UX - The loading state persists until ChatGPT responds with the tool result
- Currently available - This API actually exists and works in production
What Doesn’t Work
Section titled “What Doesn’t Work”❌ Using callTool() from Widget Results (Not Yet Available)
Section titled “❌ Using callTool() from Widget Results (Not Yet Available)”// This pattern DOES NOT work - callTool is undefinedawait window.openai.callTool("create_shopping_list", {});Problems:
callToolisundefinedin the current ChatGPT Apps SDK- TypeError thrown: “window.openai.callTool is not a function”
- Documented in OpenAI’s SDK but not yet implemented
- No ETA for when this API will become available
Note: If/when callTool() becomes available, it will be the preferred approach for direct tool invocation from widgets. Until then, use sendFollowUpMessage() as shown above.
Implementation Checklist
Section titled “Implementation Checklist”When creating interactive widget buttons:
- Use
window.openai.sendFollowUpMessage({ prompt })for ALL button interactions (both initial screens and widget results) - Use the
useAIAction()hook pattern for consistency across widgets - Provide clear, conversational prompts (e.g., “Create a shopping list for me”)
- Create a loading state (e.g.,
const [isLoading, handleAction] = useAIAction(prompt, actionName)) - Set loading to
truewhen button is clicked - Show loading UI with appropriate messaging (e.g., “Generating list below…”)
- Include
disabled={isLoading}on the button - Only set loading to
falsein error cases - Let ChatGPT handle the transition after successful completion
- Add console logging for debugging (button click, message sent, errors)
Future-proofing:
- When
callTool()becomes available, replacesendFollowUpMessage()withcallTool(toolName, args)for direct tool invocation
Examples from Production Code
Section titled “Examples from Production Code”All examples use the same useAIAction() hook pattern with sendFollowUpMessage():
Reusable Hook Pattern (shopping-list.html:361-381, user-profile.html:178-196)
Section titled “Reusable Hook Pattern (shopping-list.html:361-381, user-profile.html:178-196)”// ✅ Reusable AI action handler - used across all widgetsfunction useAIAction(prompt, actionName = 'action') { const [isLoading, setIsLoading] = useState(false);
const handleAction = async () => { console.log(`🚀 ${actionName} button clicked`); setIsLoading(true); try { console.log(`📤 Sending follow-up message: ${prompt}`); await window.openai.sendFollowUpMessage({ prompt }); console.log('✅ Follow-up message sent successfully'); } catch (error) { console.error(`❌ ${actionName} error:`, error); setIsLoading(false); } // Don't set isLoading to false - let ChatGPT handle the transition };
return [isLoading, handleAction];}Shopping List Optimize Button (shopping-list.html:412)
Section titled “Shopping List Optimize Button (shopping-list.html:412)”// ✅ Using the reusable hookconst [isOptimizing, handleOptimize] = useAIAction( "Optimize my shopping list", "Optimize Shopping List");
// In JSX:<button onClick={handleOptimize} disabled={isOptimizing}> {isOptimizing ? "Generating optimization below..." : "Optimize Shopping"}</button>User Profile Create Shopping List Button (user-profile.html:201)
Section titled “User Profile Create Shopping List Button (user-profile.html:201)”// ✅ Using the reusable hookconst [isCreatingList, handleCreateList] = useAIAction( "Create a shopping list for me", "Create Shopping List");
// In JSX:<button onClick={handleCreateList} disabled={isCreatingList}> {isCreatingList ? "Generating list below..." : "Create Shopping List"}</button>Tool Configuration
Section titled “Tool Configuration”While the button uses sendFollowUpMessage(), the tool itself should still be configured with proper metadata:
return &mcp.CallToolResult{ Result: mcp.Result{ Meta: map[string]any{ "openai/outputTemplate": widgetShoppingList, "openai/toolInvocation/invoking": "Creating your shopping list...", "openai/toolInvocation/invoked": "Shopping list ready!", "openai/widgetAccessible": true, "openai/resultCanProduceWidget": true, }, }, Content: []mcp.Content{...},}, nil