1. De ToolOutput ao componente React: fluxo geral de dados
Na aula anterior, vimos como a ferramenta no servidor forma o ToolOutput — uma resposta estruturada para o modelo e o widget. Agora vamos olhar a segunda metade desse caminho: como esse ToolOutput chega ao widget e vira UI.
Para não encarar o que acontece como mágica, vamos repassar o caminho dos dados do usuário até o seu widget. Em forma simplificada, é assim:
- O usuário faz uma pergunta no chat.
- O GPT analisa o pedido, olha a lista de ferramentas e decide: “Agora vai me ajudar suggest_gifts”.
- O GPT forma um tool call com nome e argumentos (ToolInput) e o envia para o seu servidor (MCP ou backend).
- O servidor executa a lógica da ferramenta e retorna o resultado como ToolOutput — um JSON estruturado com dados, mais um resumo textual para o modelo.
- O ChatGPT recebe o ToolOutput e o passa adiante: para o modelo (para continuar o diálogo) e para o seu widget via Apps SDK (window.openai.toolOutput ou via hooks).
- Seu widget — um componente React comum — lê o toolOutput e renderiza a UI.
Esquematicamente, fica assim:
flowchart TD U[Usuário] -->|pergunta no chat| GPT[GPT] GPT -->|callTool: suggest_gifts| B[Backend/MCP] B -->|"ToolOutput (JSON)"| GPT GPT -->|passa toolOutput| W["Widget (React)"] W -->|cartões, listas| U
É importante fixar a ideia: ToolOutput não é apenas “resposta do servidor”. Ele também é seu comando de renderização para o widget e, ao mesmo tempo, contexto para o modelo. Um bom App é aquele em que esse JSON vira uma interface conveniente, e não algo que o desenvolvedor precisa rolar com os olhos no DevTools.
2. Anatomia do ToolOutput: o que há dentro
O formato do resultado da ferramenta no Apps SDK se divide em três blocos lógicos: structuredContent, content e _meta (que chega ao widget com o nome toolResponseMetadata).
De forma aproximada, podemos representá-lo assim:
{
"structuredContent": { /* dados para UI + modelo */ },
"content": "Resumo curto em texto para o modelo e o usuário",
"_meta": { /* dados de serviço apenas para o widget */ }
}
Na tabela abaixo, veja quem enxerga o quê:
| Campo | Quem vê | Para que serve |
|---|---|---|
|
Modelo + widget | Principais dados estruturados (listas, objetos, parâmetros) |
|
Modelo + usuário (no texto) | Resumo curto que o GPT pode incluir na resposta |
|
Apenas o widget | Dados técnicos que o modelo não precisa (IDs, versões, chaves etc.) |
A documentação do Apps SDK enfatiza que a dupla structuredContent / content vai para o modelo e pode ser usada nas respostas subsequentes. O campo _meta permanece oculto e acessível apenas dentro do widget via toolResponseMetadata.
Exemplo de ToolOutput para GiftGenius
Suponha que nossa ferramenta de servidor suggest_gifts retorne algo assim:
{
"structuredContent": {
"items": [
{
"id": "boardgame-cozy-strategy",
"title": "Cozy Strategy Board Game",
"price": 39.99,
"currency": "USD",
"score": 0.92,
"tags": ["board_game","strategy","2-4_players"]
}
]
},
"content": "Encontrei algumas ideias de presentes. O widget abaixo as mostra como cartões.",
"_meta": {
"giftGenius": {
"catalogVersion": "2025-10-01",
"experimentBucket": "A"
}
}
}
Aqui, structuredContent.items é o que seu widget React vai renderizar; o content pode ser usado pelo modelo para explicar ao usuário o que está acontecendo; _meta.giftGenius é informação interna, necessária apenas para sua UI ou para analytics (por exemplo, qual versão do catálogo usar para os links).
É justamente o structuredContent o objeto que você vai observar no JSX, em vez de fazer parsing manual de um JSON arbitrário do servidor.
3. Obtendo o ToolOutput no widget: window.openai e hooks
Hora de sair da conversa sobre JSON e ir para o código. Como esse ToolOutput chega ao seu componente React?
O template do Apps SDK faz isso de duas formas principais: ou diretamente via window.openai.toolOutput, ou, o que é ótimo, por meio de hooks React prontos (useWidgetProps, useToolOutput e similares). A abordagem recomendada é usar hooks, para não mexer no window.openai diretamente e ter um código mais testável e seguro.
Opção mais simples: direto de window.openai
Para entender, veja a opção “crua”:
'use client';
function RawToolOutputDebug() {
const toolOutput = (window as any).openai?.toolOutput;
return (
<pre>{JSON.stringify(toolOutput, null, 2)}</pre>
);
}
Não faça isso em produção; porém, para depuração e “dar uma olhada” nos primeiros passos — funciona.
Opção prática: via hook do React
É bem mais conveniente envolver o acesso a window.openai em um pequeno hook e trabalhar já com um objeto tipado. Suponha que nosso SDK provisório forneça um hook useWidgetProps, retornando toolOutput e toolResponseMetadata.
'use client';
import { useWidgetProps } from '@/lib/openai-widget';
export function GiftWidgetRoot() {
const { toolOutput, toolResponseMetadata } = useWidgetProps();
// Por enquanto, só exibimos a quantidade de presentes
const items = toolOutput?.structuredContent?.items ?? [];
return (
<div>
Presentes encontrados: {items.length}
</div>
);
}
No template real, o nome do hook pode variar, mas a ideia é sempre a mesma: o SDK busca os dados em window.openai e os entrega ao seu componente como props ou via contexto. Isso é bem mais simples do que ir ao objeto global toda hora e, além disso, permite nos testes trocar facilmente a fonte de dados (por exemplo, injetar uma fixture de toolOutput).
4. Renderizando presentes: de structuredContent para JSX
Vamos à parte legal: pegar structuredContent.items e desenhar cartões a partir deles. Lembre-se de que nosso widget é um componente cliente React comum no Next.js ('use client' no topo do arquivo).
Primeiro, definimos o tipo de um presente:
type GiftItem = {
id: string;
title: string;
price: number;
currency: string;
tags?: string[];
};
Agora um pequeno componente de cartão:
function GiftCard({ gift }: { gift: GiftItem }) {
return (
<div className="gift-card">
<div className="gift-title">{gift.title}</div>
<div className="gift-price">
{gift.price} {gift.currency}
</div>
</div>
);
}
E um componente de lista que busca os dados em toolOutput:
'use client';
import { useWidgetProps } from '@/lib/openai-widget';
export function GiftList() {
const { toolOutput } = useWidgetProps();
const items = (toolOutput?.structuredContent?.items ?? []) as GiftItem[];
return (
<div className="gift-list">
{items.map(gift => (
<GiftCard key={gift.id} gift={gift} />
))}
</div>
);
}
Note como tudo aqui parece código React comum. A única “mágica” é a fonte de dados: em vez de props ou fetch, lemos o toolOutput do contêiner do ChatGPT.
E sim, não tem problema se no começo você adicionar as GiftItem[]. Depois, você pode tipar o structuredContent com cuidado por meio de tipos compartilhados com o backend (por exemplo, usar Zod / JSON Schema → tipos TS), mas para demonstração isso é suficiente.
5. Estados de UI ao redor do ToolOutput: carregando, vazio, erro
Um app que só mostra cartões quando dá sorte e fica mudo no resto do tempo não é muito amigável. É preciso tratar explicitamente, no mínimo, quatro estados: enquanto a ferramenta está executando, quando ainda não há dados, quando há resultado e quando algo deu errado.
O Apps SDK geralmente fornece alguma informação sobre o status da chamada da ferramenta: via lista de invocações de tool (useToolInvocations) ou flags associadas ao toolOutput. Para esta aula, basta um modelo simples: se o toolOutput ainda não chegou — estamos em “carregamento”; se chegou, mas a lista está vazia — “vazio”; se veio um erro — “erro”.
Para simplificar, vamos supor que o servidor, em caso de erro, coloca em structuredContent o campo error, e a flag ok na raiz do toolOutput fica false. Já discutimos esse esquema na aula anterior sobre a implementação no servidor, quando projetamos o contrato de resposta da ferramenta.
type ToolOutput = {
ok: boolean;
structuredContent?: {
items?: GiftItem[];
error?: { code: string; message: string };
};
};
Agora atualizamos nosso componente de lista:
'use client';
import { useWidgetProps } from '@/lib/openai-widget';
export function GiftListWithStates() {
const { toolOutput } = useWidgetProps() as { toolOutput?: ToolOutput };
if (!toolOutput) {
return <div>Procurando presentes…</div>;
}
if (!toolOutput.ok) {
const msg = toolOutput.structuredContent?.error?.message
?? 'Não foi possível obter as recomendações.';
return <div>Erro: {msg}</div>;
}
const items = toolOutput.structuredContent?.items ?? [];
if (items.length === 0) {
return <div>Não encontramos presentes para as suas condições. Tente ajustar os parâmetros.</div>;
}
return (
<div className="gift-list">
{items.map(gift => (
<GiftCard key={gift.id} gift={gift} />
))}
</div>
);
}
Esse código já oferece uma experiência decente ao usuário:
- Enquanto a ferramenta trabalha, dá para ver que algo está acontecendo.
- Se tudo cair — há uma mensagem compreensível, e não uma tela vazia.
- Se nada for encontrado — não fingimos que é normal; explicamos honestamente o que aconteceu.
Em produção, você provavelmente vai trocar o texto “Procurando presentes…” por um pequeno skeleton ou spinner. Para erros complexos, dá para permitir que o GPT formule uma explicação legível. Mas a estrutura básica dos componentes permanece a mesma.
6. Usando _meta e toolResponseMetadata na UI
Já aprendemos a renderizar os dados principais de structuredContent e a tratar estados básicos de loading/empty/error. Falta ainda um pedaço importante do ToolOutput de que o modelo não faz uso — o campo _meta.
Voltando ao campo _meta. Ele não é visível ao modelo, mas chega ao seu widget como toolResponseMetadata (o nome pode variar, mas a ideia é essa).
É um ótimo lugar para aquilo que não deve afetar o raciocínio do GPT, mas é importante para a UI:
- versões de catálogo ou de configuração;
- ID interno de campanha/experimento A/B;
- flags sobre quais “botões” mostrar ao usuário;
- qualquer aspecto técnico que você não quer misturar com dados de domínio.
Por exemplo, o servidor pode retornar este _meta:
"_meta": {
"giftGenius": {
"catalogVersion": "2025-10-01",
"showExperimentalBadges": true
}
}
O widget pode ler isso e, digamos, exibir um badge “Ideia nova” em alguns cartões.
type GiftMeta = {
giftGenius?: {
catalogVersion: string;
showExperimentalBadges?: boolean;
};
};
export function GiftListWithMeta() {
const { toolOutput, toolResponseMetadata } = useWidgetProps() as {
toolOutput?: ToolOutput;
toolResponseMetadata?: GiftMeta;
};
const meta = toolResponseMetadata?.giftGenius;
const items = toolOutput?.structuredContent?.items ?? [];
return (
<div>
{meta && (
<div className="catalog-version">
Catálogo de {meta.catalogVersion}
</div>
)}
<div className="gift-list">
{items.map(gift => (
<GiftCard
key={gift.id}
gift={gift}
/>
))}
</div>
</div>
);
}
O modelo não tem nada a ver com isso: ele não sabe sobre catalogVersion e showExperimentalBadges, mas a sua UI pode usá-los como quiser.
A documentação enfatiza exatamente essa separação: dados relevantes para o diálogo e o raciocínio do modelo vão em structuredContent e content; tudo o que é puramente técnico de UI vai em _meta / toolResponseMetadata.
7. Um pouco sobre os status de ToolInvocation e “Executando X…”
Enquanto a ferramenta roda, o próprio ChatGPT mostra ao usuário o que está acontecendo: no topo do chat aparece um status como “Executando GiftGenius…” ou “Conectando-se a um app externo”. Não é você exibindo essas linhas, e sim o ambiente hospedeiro do ChatGPT reagindo aos metadados da invocação da ferramenta.
Por baixo dos panos, isso é descrito com chaves técnicas do tipo _meta["openai/toolInvocation/invoking"] e _meta["openai/toolInvocation/invoked"], que sinalizam que a ação está em execução ou concluída. Esses campos são usados pela própria plataforma para exibir o status e, em geral, você não precisa mexer neles: o SDK faz isso por você no lado do servidor.
Para UX, isso traz um bônus: mesmo que o widget ainda não tenha renderizado o skeleton, o usuário já vê que o sistema está fazendo algo. Sua tarefa é complementar esse status global com estados locais como “Procurando presentes…” e um skeleton no widget, como fizemos acima.
8. Tamanho dos dados e desempenho: não empurre o mundo inteiro para structuredContent
Vale falar sobre “quanto dá para colocar em structuredContent”. Pode parecer tentador: “Tenho o catálogo completo de presentes — vou enviá-lo inteiro, e o widget filtra”. Na prática, não faça isso.
Primeiro, structuredContent entra no contexto do modelo (LLM), e o volume total de tokens é limitado. A documentação e os guias práticos recomendam fortemente manter o volume enxuto: não é um repositório de dados, e sim o resultado de uma ação.
Segundo, quanto maior o payload, mais lento o retorno e maior a chance de topar com limites ou sofrer truncamentos/erros inesperados.
Abordagem sensata:
- O backend filtra e ordena previamente, retornando exatamente o que é necessário para o passo atual: por exemplo, 10–20 melhores presentes.
- Se precisar de próximas páginas, isso é uma nova ação (novo tool call, novo ToolOutput).
- Para aspectos puramente de UI (por exemplo, a lista de possíveis tags para filtragem), você pode usar _meta, mas também com moderação.
No módulo sobre estado, já discutimos o conceito “backend — fonte da verdade; widget — cache/visualização”. Aqui é o mesmo: o resultado da ferramenta é um “recorte” cuidadoso do estado no momento da chamada, não uma cópia completa do seu banco.
9. Integração com o estado do widget e o diálogo subsequente
Embora esta aula seja oficialmente sobre ToolOutput → UI, vale lembrar que por perto existe outro pedaço importante — o widgetState. É ele que permite memorizar a escolha do usuário entre renderizações e transformar seu widget não apenas numa vitrine, mas num verdadeiro assistente ou “configurador de presentes”.
Um cenário típico:
- O primeiro ToolOutput traz a lista de presentes.
- O usuário clica em um dos cartões.
- O widget grava no widgetState qual presente foi selecionado e, possivelmente, envia um follow-up ou um novo tool call para detalhes.
- Os ToolOutput seguintes se baseiam nessa escolha.
Do ponto de vista do código, isso parece um estado React normal mais uma chamada a setWidgetState, que salva a escolha no lado do ChatGPT. A diferença é que esse estado está acessível ao modelo e ao seu backend, então precisa ser compacto e não conter segredos.
Vamos detalhar isso nos módulos sobre workflows de múltiplas etapas e follow-ups. Já agora, é útil pensar assim: o ToolOutput lhe dá um “recorte de dados” do servidor, e o widgetState — o contexto da escolha do usuário ao redor desse recorte.
Erros comuns ao trabalhar com ToolOutput → UI
Erro nº 1: “A UI renderiza a árvore JSON crua sem adaptação ao usuário”.
Às vezes dá vontade de, para depurar, fazer um <pre>{JSON.stringify(toolOutput)}</pre> e parar por aí. Para desenvolvimento, tudo bem, mas em produção o usuário vê uma estrutura da qual você se orgulha, mas que ele não entende. É importante, o quanto antes, embrulhar o structuredContent em componentes significativos (listas, cartões, tabelas), em vez de obrigar a pessoa a ler uma resposta tokenizada do servidor.
Erro nº 2: Misturar dados de domínio e metadados técnicos em structuredContent.
O código fica muito mais limpo se você separar: “o que deve ser visível ao modelo e ao usuário” e “o que é necessário apenas à UI e à analytics”. Campos técnicos — flags de experimento, versões de catálogos, idempotency key — pertencem a _meta / toolResponseMetadata. Quando tudo isso está misturado em structuredContent, fica mais difícil evoluir o contrato e testar o comportamento do modelo.
Erro nº 3: Falta de estados explícitos de carregamento, vazio e erro.
Um <div></div> vazio no lugar de “Nada encontrado” ou “Algo deu errado” é caminho certo para o usuário concluir: “O App não funciona”. Mesmo placeholders mínimos e um skeleton simples melhoram dramaticamente o UX. Não dependa apenas do status do sistema do ChatGPT “Executando X…” — o widget também deve dizer o que está acontecendo com ele.
Erro nº 4: Tentar colocar o mundo inteiro em um único ToolOutput.
Retornar o catálogo inteiro de produtos, o histórico do usuário e ainda logs do servidor dentro de um structuredContent é uma má ideia. Isso estoura limites do modelo, deixa a resposta lenta e complica a UI. É melhor retornar exatamente o volume de dados necessário para o passo atual (página de lista, detalhes do item selecionado etc.) e tratar os passos seguintes como novas chamadas de ferramenta.
Erro nº 5: Acoplamento rígido da UI a uma forma instável de resposta, sem tipos.
Se, em todo lugar do código, você escrever toolOutput.structuredContent.items[0].whatever, sem verificar a existência de campos e sem tipos, qualquer evolução do schema no servidor vai derrubar o widget. Vale a pena sincronizar tipos com JSON Schema (geração de tipos TS) ou, pelo menos, descrever manualmente as interfaces (GiftItem, ToolOutput) e trabalhar com campos opcionais com cuidado.
Erro nº 6: Ignorar _meta e sobrecarregar o modelo com campos “extras”.
É tentador enfiar em structuredContent tudo o que vem à mente, porque “é JSON, não faz mal”. Mas cada campo aumenta o contexto do modelo, e muitos deles não são necessários ao modelo. Se a informação não deve afetar o raciocínio do GPT e não precisa ir para a resposta textual, coloque-a em _meta e trabalhe com ela apenas no widget.
Erro nº 7: Acessos diretos a window.openai a partir de uma dezena de componentes.
Sim, window.openai.toolOutput funciona, mas quando metade do app começa a acessar a variável global, depurar e testar vira um inferno. É muito melhor embrulhar isso uma vez em um hook/contexto (useWidgetProps/useToolOutput) e, depois, usar props normais e objetos tipados. É mais limpo e mais fácil de substituir por fixtures no Storybook/testes.
GO TO FULL VERSION