Skip to content

Widget Button Pattern for ChatGPT Apps SDK

Widget Button Pattern for ChatGPT Apps SDK

Section titled “Widget Button Pattern for ChatGPT Apps SDK”

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

⚠️ 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 undefined
const result = await window.openai.callTool("create_shopping_list", {});

Evidence:

  • Console checks show typeof window.openai.callTool === 'undefined'
  • Current production widget implementations use sendFollowUpMessage instead
  • Local test mocks only implement sendFollowUpMessage, not callTool

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

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 sendFollowUpMessage
function 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 component
const [isCreatingList, handleCreateList] = useAIAction("Create a shopping list for me", "Create Shopping List");
  1. Uses sendFollowUpMessage() - Current working API for widget buttons
  2. Conversational prompts - Sends natural language prompts that ChatGPT interprets
  3. Loading state management - Sets loading to true, but does NOT set it to false (ChatGPT handles the transition)
  4. Error handling - Only sets loading to false if there’s an error
  5. Reusable hook pattern - useAIAction() makes implementation consistent across widgets
  • 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

❌ 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 undefined
await window.openai.callTool("create_shopping_list", {});

Problems:

  • callTool is undefined in 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.

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 true when button is clicked
  • Show loading UI with appropriate messaging (e.g., “Generating list below…”)
  • Include disabled={isLoading} on the button
  • Only set loading to false in 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, replace sendFollowUpMessage() with callTool(toolName, args) for direct tool invocation

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 widgets
function 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 hook
const [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 hook
const [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>

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