Involuntary re rendering of widget when using sendFollowUpMessage

Hello.

So somehow (without me changing anything of the code logic) when a widget uses sendFollowUpMessage to render a new widget.. chatgpt is re rendering the main widget and then it renders the new one… this is a new behavior cuz before last week it wasn’t happening with the same codebase. anyone facing the same situation?

thanks!

2 Likes

for me, sendFollowUpMessage never triggers a new tool call = so no new widget (and i would like that). It just triggers textual response or thinking block.

1 Like

Hi Strajk,

What if, when you click the card, you instead open the widget in full screen and show the details of that city?

From what I recall, when I tried the GitHub - openai/openai-apps-sdk-examples: Example apps for the Apps SDK, this is how their pizza example works.

For deterministic outcomes (like when you click a card and 100% of the time want to be shown the details), it’s best to use code, not LLM.

I mean, it depends what you tell it to do. In my case I would do something like this:

await window.openai.sendFollowUpMessage({
prompt: `Show me the service form for ${serviceKey}. Please use the service-form tool.`
});

and this was working fine last time i checked.

how do you render a different widget from a widget?? thanks!

Just started a thread about this same thing today. @Diego_Quiroz

Did you ever get any clarification on this?

It feels like a bug to me, maybe related to dev mode, but I’m not sure. I’ve tried a lot of things but it always remounts when you use sendFollowUpMessage which is weird because it seems quite clear they want the widgets to add to the chat not take away from the flow of conversation.

1 Like

i ended up just using 1 widget and re rendering different components inside of the same widget.

1 Like

@Diego_Quiroz and @duncansmothers Same issue here. When I call window.openai.callTool() to update my widget and then use sendFollowUpMessage to notify ChatGPT of the changes, it’s triggering a new tool call that creates a duplicate widget below with the original data.

The widget updates correctly in place from callTool, but any follow-up message—even with system-style formatting like “[System] Widget data updated”—causes ChatGPT to re-invoke the tool and render a new widget.

Without sendFollowUpMessage, ChatGPT doesn’t have context of the widget updates for follow-up questions. Have you found a workaround, or is this a bug on OpenAI’s side?

1 Like

The fix that worked for me was that ChatGPT wasn’t recognizing my tool hints.

FastMCP was storing the parameters in meta in its own generic container, but it does not automatically map them to the specific fields that the MCP protocol and OpenAI’s dashboard expect.

So it was working for a lot of things but leading to that error.

The fix, for me, was to manually place those hints in the Standard MCP Annotations placed in the annotations field without any prefix.

Before that, ChatGPT was assuming the defaults for readOnlyHint=False, openWorldHint=True, and destructiveHint=True, leading to that weird flow.

Hey Everyone, Can someone please confirm if the workaround suggested by @duncansmothers work in resolving the issue? Thank you!

I’ve created this document so you understand the issue more in detail

sendFollowUpMessage Tool Re-Invocation: Experimental Analysis

Executive Summary

This document presents a comprehensive experimental analysis of the sendFollowUpMessage SDK behavior in ChatGPT Apps, specifically investigating whether tool re-invocation can be prevented through message phrasing, timing isolation, or prompt engineering techniques.

Key Finding: sendFollowUpMessage always triggers ChatGPT’s tool selection logic, regardless of message content, phrasing, or timing. No workaround exists to prevent this behavior within the current SDK design.


Background: The Core Issue

Problem Statement

When using window.openai.sendFollowUpMessage({ prompt: string }) in a ChatGPT App widget, the SDK inserts a message into the conversation “as if the user had typed it manually.” However, this behavior has an unintended consequence:

ChatGPT analyzes the message content and autonomously decides whether to invoke available tools, even when:

  • The widget developer did not intend to trigger a tool
  • The message explicitly instructs ChatGPT NOT to call tools
  • Tool hints indicate the tool is idempotent and read-only
  • The tool was just called moments before

Initial Hypothesis

Original Theory: The tool re-invocation might be timing-dependent—occurring only when sendFollowUpMessage is called immediately after window.openai.callTool.

Test Premise: If we isolate sendFollowUpMessage from any explicit tool invocations and craft messages carefully, we might be able to send update summaries without triggering tool re-execution.


Experimental Design

Test Environment Setup

Isolation Strategy:

  • Created a dedicated test button completely separate from the widget’s recalculation logic
  • Test button invokes sendFollowUpMessage WITHOUT calling window.openai.callTool first
  • Eliminates any timing dependency between tool invocation and follow-up message

Tool Configuration:

# Tool annotations configured per OpenAI best practices
"idempotentHint": True,      # Same inputs = same outputs
"readOnlyHint": True,         # No side effects
"destructiveHint": False,     # Non-destructive operation
"openWorldHint": False        # Deterministic behavior

Widget State Exposure:

  • All updated values available to ChatGPT via window.openai.setWidgetState()
  • ChatGPT has access to widget data without needing to call tools

Eight Experimental Message Variations

Each experiment tested a different hypothesis about what might prevent tool re-invocation:

Experiment 1: Direct Instruction Format

"Please summarize the changes without recalculating or calling any tools"

Hypothesis: Explicit instruction not to call tools should be respected
Rationale: Direct commands might override ChatGPT’s default tool selection behavior

Experiment 2: Question Format

"Can you summarize what changed in the retirement outlook based on the widget state?"

Hypothesis: Phrasing as a question + reference to existing widget state avoids tool triggers
Rationale: Questions might be interpreted as requests for information already available

Experiment 3: Past-Tense Statement

"The user just updated their retirement age to ${age}. Here's what changed."

Hypothesis: Past-tense framing indicates action already completed
Rationale: Completed actions shouldn’t need re-execution

Experiment 4: Minimal Factual Statement

"Updated retirement age: ${age}"

Hypothesis: Minimal text with no prompt-like language avoids triggering analysis
Rationale: Shorter messages with fewer semantic cues might bypass tool selection

Experiment 5: Widget State Reference

"Widget state updated with new retirement age ${age}. Values available via getWidgetState."

Hypothesis: Explicit mention of data availability mechanism prevents tool lookup
Rationale: Directing ChatGPT to existing data source eliminates need for tool call

Experiment 6: Tool Already Called Declaration

"The retirement-outlook tool was just called and returned updated values. The retirement age is now ${age} with projected monthly income of $${income}. Can you summarize what this means?"

Hypothesis: Stating tool was already executed prevents redundant invocation
Rationale: ChatGPT might recognize redundancy and skip re-execution

Experiment 7: Informational Update

"FYI: Retirement calculations refreshed. New target age: ${age}"

Hypothesis: “FYI” framing signals informational-only message
Rationale: Non-actionable tone might prevent tool selection logic from triggering

Experiment 8: Pure Data Broadcast

"Data update: retirementAge=${age}, monthlyIncome=${income}"

Hypothesis: Key-value format without natural language avoids semantic analysis
Rationale: Structured data might not trigger conversational AI’s tool selection


Test Execution & Results

Testing Methodology

  1. Widget rendered successfully with test button visible
  2. User clicked test button to trigger isolated sendFollowUpMessage call
  3. No explicit callTool invocation occurred before or after the message
  4. Observed ChatGPT’s response and any tool invocations

Findings: Experiments 1 & 2

Experiment 1 Result: :cross_mark: FAILED

  • ChatGPT attempted to invoke the retirement-outlook tool
  • Resulted in 404 error (tool not found by name)
  • Connection failure: “Stopped talking to connector”

Experiment 2 Result: :cross_mark: FAILED

  • ChatGPT attempted to invoke the retirement-outlook tool
  • Same 404 error pattern
  • Connection failures and template fetch errors

Critical Observation

Both experiments failed identically, despite:

  • :white_check_mark: No timing relationship with callTool
  • :white_check_mark: Explicit instructions NOT to call tools
  • :white_check_mark: Reference to widget state as data source
  • :white_check_mark: Tool configured with idempotentHint: true

This definitively disproves the timing-dependency hypothesis.

Error Pattern Analysis

Consistent Error Sequence:

  1. sendFollowUpMessage sends message to ChatGPT
  2. ChatGPT’s internal tool selection logic analyzes message
  3. ChatGPT decides a tool invocation is needed
  4. Tool invocation fails (404: tool name mismatch or unavailable)
  5. Connection to widget/connector drops
  6. User sees error state

Root Cause:
ChatGPT’s tool selection operates independently of:

  • Developer intent
  • Explicit instructions in message content
  • Tool hints/annotations
  • Timing of previous tool calls
  • Available widget state data

Comprehensive Code Audit Findings

Verification Checklist

:white_check_mark: Tool Annotations: Properly configured with idempotentHint: true, readOnlyHint: true
:white_check_mark: No Code-Based Tool References: All sendFollowUpMessage calls contain only text prompts
:white_check_mark: Proper SDK Usage: No malformed API calls or incorrect parameters
:white_check_mark: State Management: Widget state properly exposed via setWidgetState()
:white_check_mark: Isolation Confirmed: Test button has zero interaction with callTool logic

Message Content Analysis

Even Experiment 6, which explicitly stated “The retirement-outlook tool was just called and returned updated values,” triggered re-invocation.

Key Insight: The mention of the tool name in the message text was not a code invocation—just descriptive text. However, ChatGPT’s semantic analysis treated it as a signal to call the tool anyway.


Conclusions

Primary Conclusion

sendFollowUpMessage cannot be used for post-tool-call summaries or updates without triggering tool re-invocation.

This is not a timing issue, configuration issue, or prompt engineering problem—it’s a fundamental characteristic of how the SDK operates.

Why This Happens

Per OpenAI’s documentation, sendFollowUpMessage treats messages “as if the user asked it.” This means:

  1. Message enters normal conversational flow
  2. ChatGPT’s tool selection logic activates (standard behavior for user messages)
  3. Semantic analysis occurs independent of developer intent
  4. Tool invocation decision is autonomous based on ChatGPT’s interpretation
  5. No SDK mechanism exists to bypass this behavior

Implications

What Works:

  • :white_check_mark: window.openai.setWidgetState() - Safely exposes data without triggering tools
  • :white_check_mark: window.openai.callTool() - Explicit, controlled tool invocation
  • :white_check_mark: Widget UI updates - Direct DOM manipulation, no ChatGPT interaction

What Doesn’t Work:

  • :cross_mark: Using sendFollowUpMessage to narrate recent changes
  • :cross_mark: Sending “FYI” updates after tool calls
  • :cross_mark: Any attempt to prompt ChatGPT without risking tool re-invocation

Recommended Approach

For Post-Tool-Call Updates:

  1. Use setWidgetState() to expose new data
  2. Let ChatGPT access data on-demand if user asks questions
  3. Avoid sendFollowUpMessage entirely for automated updates
  4. Reserve sendFollowUpMessage ONLY for genuine user-initiated prompts (e.g., CTA buttons)

For User-Initiated Actions:

  • CTA buttons that send predefined prompts: :white_check_mark: Acceptable use
  • These ARE intended to trigger new conversations and potential tool calls

Technical Recommendations for OpenAI

Requested SDK Enhancement

Feature Request: sendFollowUpMessage option to prevent tool invocation

Proposed API:

window.openai.sendFollowUpMessage({
  prompt: string,
  allowToolCalls?: boolean  // Default: true
});

Use Case:
Widgets that need to send contextual updates or summaries to ChatGPT without triggering re-execution of expensive or redundant tool calls.

Alternative Solution:
Provide a separate SDK method like sendContextUpdate() specifically for informational messages that should never trigger tools.

Workaround Request

If API enhancement is not feasible, official documentation should:

  1. Explicitly warn that sendFollowUpMessage ALWAYS enables tool selection
  2. Clarify that tool hints do NOT prevent re-invocation via follow-up messages
  3. Provide guidance on when to use setWidgetState() vs. sendFollowUpMessage

Appendix: Test Code Reference

Isolated Test Implementation

const handleTestFollowUpMessage = useCallback(async () => {
  if (!window.openai?.sendFollowUpMessage) {
    return;
  }

  // NO callTool invocation - completely isolated test
  const age = displayData.retirementAgeGoal;
  const income = displayData.projectedMonthlyRetirementIncome;

  // Eight experimental message variations tested here
  const experiment1 = "Please summarize the changes without recalculating or calling any tools";
  const experiment2 = "Can you summarize what changed in the retirement outlook based on the widget state?";
  // ... experiments 3-8 ...

  try {
    await window.openai.sendFollowUpMessage({ 
      prompt: experiment1  // Or any other experiment
    });
  } catch (error) {
    console.error('sendFollowUpMessage failed:', error);
  }
}, [displayData]);

Configuration Verification

# Tool annotations from base.py:137
def get_annotations(self) -> Dict[str, Any]:
    return {
        "idempotentHint": True,      # Confirmed present
        "readOnlyHint": True,         # Confirmed present
        "destructiveHint": False,     # Confirmed present
        "openWorldHint": False        # Confirmed present
    }

Document Metadata

Test Date: January 2026
SDK Version: ChatGPT Apps SDK (Current production version)
Testing Environment: Production ChatGPT interface with MCP-based widget
Experiments Conducted: 8 message variations
Experiments Tested by User: 2 (both failed)
Code Audit Scope: Complete project verification

Status: :white_check_mark: Analysis Complete | :cross_mark: No Workaround Found | :clipboard: Feature Request Pending