1. O que é o contexto do workflow e por que ele é necessário
Em um aplicativo web comum, você tem uma ideia bem clara de onde vive o estado: banco de dados, cache e algo no front-end como Redux ou o estado local do React. No ChatGPT App é mais divertido: o estado fica espalhado por três mundos ao mesmo tempo — dentro do modelo (histórico do diálogo), dentro do widget (estado de UI) e no seu servidor/MCP (dados de negócio).
Por contexto do workflow vamos entender todo o conjunto de dados necessários para responder às perguntas “em que etapa estamos” e “o que já se sabe”. Falando do nosso app didático GiftGenius, o contexto inclui:
- perfil do destinatário do presente: idade, gênero, interesses;
- orçamento e, possivelmente, a moeda;
- lista de ideias geradas e quais delas o usuário curtiu ou ocultou;
- itens técnicos: identificador da sessão ou do workflow, status (“profile_collected”, “ideas_shown”, “checkout_started”).
Esse contexto é necessário não só para você como desenvolvedor de backend. Ele é necessário para o próprio modelo, para que entenda quais perguntas já foram feitas, quais ferramentas já foram chamadas e sobre o que estamos falando agora. E é necessário para o usuário, para que, ao voltar ao chat, não precise começar tudo do zero.
O usuário intuitivamente pensa que “o ChatGPT lembra de tudo”. Na prática, o modelo lembra apenas o texto do diálogo, e enquanto ele couber na janela de contexto. Coisas estruturadas como order_id, cart_id ou “lista de ideias curtidas” precisam ser armazenadas no seu servidor, do contrário você terá uma máquina perfeita para gerar afirmações confiantes, porém incorretas.
2. Três níveis de estado: UI, LLM e negócio
É mais fácil entender a preservação do contexto por meio de um modelo de três camadas de estado. Também chamado de “State Triad”.
Tabela de níveis
Vamos usar uma pequena tabela:
| Nível | Onde fica | Ciclo de vida | Responsabilidade | Exemplo no GiftGenius |
|---|---|---|---|---|
| UI State | Widget (React, widgetState) | Enquanto o chat/mensagem com o widget está aberto | Estado visual, entrada local | Quais cartões estão destacados, estado do formulário |
| LLM Context | Histórico do chat no OpenAI | Enquanto a mensagem “cabe” no contexto | Compreensão do diálogo e raciocínio | “Estamos buscando um presente para a mãe, orçamento de $50” |
| Business State | MCP / seu backend (BD/Redis) | Quanto tempo você quiser (persistente) | Fonte da verdade: dados verificados, status | { step: "ideas", budget: 50, liked: [42, 51] } |
A camada de UI é rápida e responsiva, mas muito frágil: o ChatGPT pode “desmontar” o iframe com o widget quando você rola para cima no histórico e depois montá-lo novamente. É para isso que existe o widgetState, que vive um pouco mais do que o componente React e é sincronizado com o host do ChatGPT.
A camada do LLM dá ao modelo a sensação de um diálogo contínuo, mas armazena apenas texto e chamadas de ferramentas. Você pode colocar ali um JSON com seu carrinho, mas isso é basicamente inserir o JSON no texto — o modelo não vai tratar isso como um banco de dados.
A camada de negócio é o que você, como engenheiro, pode controlar: lá ficam dados validados, índices e status de pedidos. Assim que você tiver um roteiro sério (presentes, reservas, ensino), essa camada deve se tornar a principal fonte da verdade sobre o estado.
O principal problema de engenharia é evitar que essas três camadas se desalinhem. O usuário mudou o orçamento no widget, o modelo ainda pensa no antigo e no banco há um terceiro valor — essa é a receita clássica para um comportamento estranho.
3. O que exatamente salvamos: a estrutura do WorkflowContext
Para falar de forma concreta, vamos descrever em TypeScript a interface de contexto do GiftGenius. Suponha que já temos algumas etapas: coleta de perfil, escolha de orçamento, geração de ideias e visualização/curtidas.
Comecemos com uma estrutura simples:
// backend/types/workflow.ts
export type GiftWorkflowStep =
| "profile"
| "budget"
| "ideas"
| "checkout";
export interface GiftWorkflowContext {
id: string; // workflowId — identificador do cenário
userId?: string; // se a autenticação já estiver configurada
currentStep: GiftWorkflowStep;
profile?: {
age?: number;
gender?: string;
interests?: string[];
};
budget?: {
min?: number;
max?: number;
currency: string;
};
ideas?: {
id: string;
title: string;
}[];
likedIdeaIds: string[];
hiddenIdeaIds: string[];
updatedAt: number; // timestamp para TTL/limpeza
}
Não é um schema final, mas os elementos importantes já estão no lugar. Temos:
- um identificador do workflow, pelo qual vamos buscar esse contexto;
- a etapa atual, que ajudará tanto o widget quanto o modelo a entender até onde chegamos;
- um conjunto de campos que serão preenchidos em etapas específicas;
- campos de serviço como o horário da atualização.
Um comentário à parte sobre identificadores. Nesta aula, por workflowId entendemos o identificador de um roteiro específico dentro do nosso backend/MCP. Ele pode coincidir com o identificador da sessão de diálogo do ChatGPT (sessionId), mas não contamos com isso. userId é o identificador do usuário do seu sistema de autenticação (se houver); um usuário pode ter vários workflows ativos. No campo id está justamente esse workflowId, pelo qual buscamos e atualizamos o contexto.
Nas próximas seções, vamos analisar três coisas: onde armazenar esses objetos, como gravá-los lá e como recuperá-los — tanto no widget quanto no modelo.
4. Onde armazenar o estado: opções e compromissos
É útil pensar na persistência do estado em duas dimensões: onde ele é armazenado e por quanto tempo vive. Nesta seção, vamos focar no local de armazenamento e voltar aos prazos de vida no checklist e no bloco de erros comuns.
Primeiro, vamos entender os locais de armazenamento.
Dentro do diálogo (no prompt)
Às vezes dá vontade de dizer: “Vamos simplesmente devolver à modelo, toda vez, um JSON com o estado atual e deixá-la se virar”. Isso funciona para roteiros muito simples e cadeias curtas de etapas, mas esbarra rapidamente em dois problemas: limite de tamanho do contexto e ausência de qualquer garantia de integridade dos dados.
Além disso, o protocolo MCP é, por natureza, stateless: como o HTTP, ele não mantém estado entre requisições por padrão. Para vincular a chamada de uma ferramenta a uma sessão específica, você precisa passar explicitamente um identificador — o workflowId ou session id — nos argumentos da ferramenta ou por metadados/cabeçalhos.
Portanto, manter o estado de negócio apenas no diálogo é mais um experimento didático do que uma arquitetura.
No widget: UI + widgetState
No nível da UI usamos o estado usual do React (useState, useReducer e assim por diante), mas, como já foi dito, o componente pode ser desmontado. No Apps SDK existe o mecanismo widgetState, que vive fora do React e é sincronizado com o host do ChatGPT. Se, ao montar o widget, você buscar o valor salvo e, ao ocorrerem mudanças, salvá-lo de volta, você terá um armazenamento local, porém bem conveniente.
Esse armazenamento é ótimo para estado puramente visual: quais cartões estão recolhidos, em qual aba você está, o que o usuário digitou no formulário antes de clicar em “Avançar”. Mas ele não substitui o servidor: assim que o usuário abrir o chat em outro dispositivo ou depois de uma semana, o widgetState pode já não ajudar. E construir a lógica de negócio sobre ele — é discutível.
No servidor/MCP: Map, Redis, BD
Por fim, a opção principal para produção: armazenamos o GiftWorkflowContext no lado do servidor MCP ou de um serviço de backend. Como o cliente e o servidor MCP são stateless pelo protocolo, devemos encaminhar o workflowId (ou state_token) em cada chamada de ferramenta para entender qual contexto atualizar.
Há várias opções de implementação:
- Map in-memory no Node.js — adequado para demos e ambiente de desenvolvimento: é tudo rápido, mas desaparece no restart;
- Redis ou outro cache in-memory com TTL — bom para roteiros tipo wizard (assistentes de algumas etapas): vive por uma ou duas horas e depois pode ser removido;
- um banco SQL/NoSQL comum — obrigatório para roteiros do tipo “voltei uma semana depois” ou “rascunhos e carrinhos”.
Nesta aula não vamos aprofundar em um banco específico; vamos focar na interface e no entendimento do que exatamente deve ir para lá.
5. O storage mais simples no servidor MCP: Map por workflowId
Vamos começar com algo simples: uma Map in-memory no servidor MCP, em que a chave é o workflowId. Em um demo didático, você pode simplesmente igualá-lo ao sessionId do diálogo, mas em produção é melhor manter o workflowId como um identificador separado do roteiro. O valor nessa Map será o GiftWorkflowContext. No mundo real, você trocará isso por Redis ou um BD, mas a API continuará a mesma.
Suponha que nosso servidor MCP seja em TypeScript. Adicione perto da inicialização:
// mcp/workflowStore.ts
import { GiftWorkflowContext } from "../backend/types/workflow";
const workflows = new Map<string, GiftWorkflowContext>();
export function getWorkflow(id: string): GiftWorkflowContext | undefined {
return workflows.get(id);
}
export function saveWorkflow(ctx: GiftWorkflowContext): void {
workflows.set(ctx.id, { ...ctx, updatedAt: Date.now() });
}
Em seguida, a ferramenta que salva o perfil do destinatário. Importante: ela recebe o workflowId e os dados do perfil e, internamente, atualiza/cria o contexto correspondente:
// mcp/tools/setProfile.ts
import { jsonSchema } from "@modelcontextprotocol/sdk"; // alias
import { getWorkflow, saveWorkflow } from "../workflowStore";
export const setProfileTool = {
name: "gift_set_profile",
description: "Salva o perfil do destinatário do presente",
inputSchema: jsonSchema.object({
workflowId: jsonSchema.string(),
age: jsonSchema.number().optional(),
gender: jsonSchema.string().optional(),
interests: jsonSchema.array(jsonSchema.string()).optional()
}),
async run(input: any) {
const existing = getWorkflow(input.workflowId);
const ctx = existing ?? {
id: input.workflowId,
currentStep: "profile",
likedIdeaIds: [],
hiddenIdeaIds: []
};
ctx.profile = {
age: input.age,
gender: input.gender,
interests: input.interests ?? []
};
ctx.currentStep = "budget";
saveWorkflow(ctx);
return {
structuredContent: {
type: "profileSaved",
workflowId: ctx.id,
profile: ctx.profile,
nextStep: ctx.currentStep
}
};
}
};
Essa ferramenta já resolve duas tarefas: salva o perfil e avança o currentStep para a próxima etapa. Em um projeto real, você talvez queira separar as ferramentas “salvar dados” e “ir para a etapa”, mas para entender o conceito, esta opção serve.
Note o workflowId nos argumentos: é esse parâmetro que vincula a chamada da ferramenta ao contexto certo. A parte cliente (widget ou agente) precisa armazená-lo em algum lugar e repassá-lo.
6. Integração com o Apps SDK: de onde obter workflowId e sessionId
A pergunta “de onde vem o workflowId” em ChatGPT Apps é um pouco filosófica. As possibilidades dependem de você usar autenticação, MCP diretamente ou o Agents SDK. Em linhas gerais, as opções são: gerar no lado do servidor na primeira chamada de ferramenta ou gerar no widget e enviar para baixo.
Para o exemplo didático, vamos supor que a primeira etapa é a chamada de uma ferramenta MCP que cria o workflow, e o widget depois apenas consome seu id.
A opção mais simples:
// mcp/tools/startWorkflow.ts
import { randomUUID } from "crypto";
import { saveWorkflow } from "../workflowStore";
export const startWorkflowTool = {
name: "gift_start_workflow",
description: "Cria um novo workflow de seleção de presentes",
inputSchema: { type: "object", properties: {} },
async run() {
const id = randomUUID();
saveWorkflow({
id,
currentStep: "profile",
likedIdeaIds: [],
hiddenIdeaIds: [],
updatedAt: Date.now()
});
return {
structuredContent: {
type: "workflowStarted",
workflowId: id,
currentStep: "profile"
}
};
}
};
Depois disso, o modelo, ao receber o workflowId na resposta da ferramenta, pode:
- registrá-lo de forma oculta no contexto;
- passá-lo ao widget por meio de structuredContent, para que o widget salve esse valor no widgetState e passe a incluí-lo nas próximas chamadas de ferramentas.
No lado do widget, o código será mais ou menos assim.
7. Armazenando o workflowId e o estado de UI local no widget
Suponha que temos um widget de lista de ideias, que precisa saber qual workflow ele exibe e lembrar as curtidas locais, mesmo se o componente for desmontado. Em uma versão simplificada:
// app/widgets/GiftIdeasWidget.tsx
import { useEffect, useState } from "react";
interface Idea {
id: string;
title: string;
}
interface WidgetProps {
widgetId: string;
workflowId: string; // veio de structuredContent
ideas: Idea[];
}
interface UiState {
liked: string[];
}
export function GiftIdeasWidget(props: WidgetProps) {
const [uiState, setUiState] = useState<UiState>({ liked: [] });
useEffect(() => {
window.openai.getWidgetState<UiState>(props.widgetId).then(saved => {
if (saved) setUiState(saved);
});
}, [props.widgetId]);
function toggleLike(id: string) {
const exists = uiState.liked.includes(id);
const next: UiState = {
liked: exists
? uiState.liked.filter(x => x !== id)
: [...uiState.liked, id]
};
setUiState(next);
window.openai.setWidgetState(props.widgetId, next);
// aqui também é possível chamar a MCP-tool "gift_like_idea"
}
return (
<ul>
{props.ideas.map(idea => (
<li key={idea.id}>
{idea.title}
<button onClick={() => toggleLike(idea.id)}>
{uiState.liked.includes(idea.id) ? "★" : "☆"}
</button>
</li>
))}
</ul>
);
}
Aqui o widgetState é usado exatamente como uma camada de UI: lembramos quais ideias estão destacadas. Idealmente, as curtidas também devem ser enviadas ao servidor (por uma ferramenta MCP ou um endpoint de API no Next.js), para que a camada de negócio também saiba o que o usuário escolheu.
É importante não tentar construir todo o workflow em cima do widgetState. Ele deve ser uma camada adicional ao contexto de negócio no servidor.
8. Restauração do roteiro: o usuário voltou
Agora vamos a um caso mais interessante: o usuário fechou o ChatGPT, voltou depois de algumas horas ou dias e abriu novamente o mesmo chat. O que deve acontecer?
O UX ideal é: o modelo e o App entendem que o usuário já tem um workflow não finalizado, trazem o contexto dele e dizem algo como: “Você já informou o perfil e o orçamento, vamos continuar com a escolha das ideias”.
Arquiteturalmente, isso fica assim:
- No seu servidor está armazenado o GiftWorkflowContext, vinculado a algum userId ou pelo menos a um workflowId interno.
- Em uma nova requisição (ou na primeira chamada de ferramenta no contexto do diálogo), o App consulta o servidor perguntando: “Existe um workflow ativo para este usuário?”.
- Se existir, o servidor o devolve e, talvez, uma flag especial resume, que o modelo usa em sua réplica.
Em um demo monolítico simples, você pode considerar que o servidor MCP e o aplicativo Next.js vivem no mesmo repositório (ou até no mesmo processo), então simplesmente reutilizamos o mesmo workflowStore do MCP também nas rotas de API.
No Next.js isso pode ser uma rota de API simples:
// app/api/gift/workflow/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getWorkflow } from "@/mcp/workflowStore"; // neste demo, MCP e Next.js compartilham o mesmo armazenamento
export async function GET(req: NextRequest) {
const id = req.nextUrl.searchParams.get("workflowId");
if (!id) return NextResponse.json({ error: "Missing workflowId" }, { status: 400 });
const ctx = getWorkflow(id);
if (!ctx) return NextResponse.json({ exists: false });
return NextResponse.json({
exists: true,
context: ctx
});
}
O widget (ou a ferramenta MCP) pode chamar esse endpoint quando precisar atualizar o estado: por exemplo, na primeira montagem ou ao alternar a etapa. Na configuração didática, basta a combinação workflowId + storage em Map; em produção real, você adicionará autenticação e verificação de propriedade do usuário.
Se você usa o Agents SDK ou uma orquestração mais complexa, pode ampliar a ideia para “checkpoints” — salvar o estado ao final de grandes etapas, de onde o agente pode continuar ao reiniciar. Mas isso já é tema do próximo módulo.
9. Avançar e voltar e histórico de etapas
Inevitavelmente surge a pergunta: “É possível voltar uma etapa?”. Para o usuário isso é muito natural: alterar o orçamento, ajustar interesses, remover um item da seleção.
Tecnicamente, isso significa duas coisas:
- é preciso armazenar não só a etapa atual, mas também o histórico de decisões tomadas;
- é preciso recalcular com cuidado os dados derivados após o rollback.
Uma das opções é adicionar ao contexto o campo history, que conterá snapshots das etapas. Por exemplo:
export interface StepSnapshot {
step: GiftWorkflowStep;
payload: any; // dados específicos da etapa
createdAt: number;
}
export interface GiftWorkflowContext {
// ...campos anteriores
history: StepSnapshot[];
}
Quando o usuário preenche o perfil, você adiciona ao histórico um snapshot com step: "profile". Quando altera o orçamento — mais um snapshot. Ao fazer rollback para o perfil, você:
- atualiza currentStep = "profile";
- opcionalmente corta o histórico até o índice necessário;
- recalcula os valores derivados (por exemplo, limpa ideias e curtidas, se dependerem do orçamento).
No nível do modelo, é importante sincronizar: se o usuário clicou no botão “Voltar” no widget, é preciso enviar uma chamada de ferramenta que atualize o contexto de negócio e retorne na resposta uma descrição explícita do novo estado. Caso contrário, você terá o problema clássico de dessincronização: a UI mostra a etapa 2, o modelo tem certeza de que você está na etapa 3.
No nível do widget, o rollback pode parecer um botão simples:
async function goBackToProfile() {
await fetch("/api/gift/workflow/back", {
method: "POST",
body: JSON.stringify({ workflowId, targetStep: "profile" })
});
// atualizamos a UI e limpamos o estado local
}
E então o servidor decide exatamente o que limpar no contexto e que mensagem enviar ao modelo por meio da resposta da ferramenta.
10. Como conectar tudo isso ao modelo: contexto para o raciocínio
Tudo o que fazemos com o estado, no fim, é necessário não apenas para o usuário, mas também para o LLM. O modelo deve entender:
- o que já se sabe (por exemplo, o perfil do destinatário e o orçamento);
- quais etapas já foram concluídas;
- se há processos não finalizados.
A forma de entregar essa informação ao modelo depende da arquitetura do App: você pode injetá-la no system prompt, devolvê-la no ToolOutput de forma estruturada ou usar campos especiais _meta/annotations, se o SDK os suportar.
Um padrão típico é:
- A ferramenta MCP retorna no structuredContent um snapshot curto do contexto: etapa atual, campos-chave e, possivelmente, o workflowId.
- O Apps SDK transforma isso em um widget ou em texto + dados ocultos.
- O modelo, ao ver o structuredContent, entende que o roteiro foi retomado e constrói a próxima ação a partir disso.
Em alguns casos, se o modelo “esqueceu” parâmetros importantes ou começou a alucinar, você pode atualizar o contexto à força: chamar uma ferramenta especial que devolva o estado atual e o modelo “entra novamente no contexto”.
É importante não tentar enfiar no modelo todo o GiftWorkflowContext até o último campo. Bastam os pontos-chave: para quem estamos buscando o presente, qual é o orçamento, quantas ideias já foram exibidas e se há um checkout não finalizado.
11. Mini checklist ao projetar o WorkflowContext
Antes de passar aos erros típicos, é útil formular um pequeno conjunto de perguntas às quais você deve responder ao projetar o contexto do workflow (você pode literalmente anotar isso ao lado da interface):
- Quais etapas o roteiro tem e qual é o conjunto mínimo de dados necessário em cada uma?
Isso o protegerá de JSONs monstruosos “por via das dúvidas”. - O que precisa ser lembrado apenas dentro de um chat e o que precisa ser lembrado entre sessões e dispositivos?
O primeiro pode ficar no widgetState e nos prompts; o segundo deve obrigatoriamente ir para o banco de dados do servidor. - Como será o identificador do contexto?
Pode ser a combinação userId + scenario, um workflowId separado ou ambos. O principal é que você consiga encontrar o contexto de forma inequívoca no banco. - Como você vai limpar workflows antigos?
Para demos, “nunca limpar” pode ser aceitável, mas em produção você vai precisar de TTL ou de jobs em background que removam workflows antigos. - O usuário precisa voltar atrás e como você vai implementar isso?
Você vai armazenar uma árvore de ramificações ou basta uma lista linear de etapas com possibilidade de rollback.
E por fim: tente simular mentalmente o cenário “o usuário voltou depois de uma semana em outro chat”. Se você não consegue explicar como o App descobrirá o workflow antigo e o que deve mostrar, é preciso reforçar a parte de armazenamento persistente.
12. Erros típicos ao trabalhar com o contexto entre etapas
Erro nº 1: armazenar tudo apenas no histórico do diálogo.
Às vezes surge a tentação: “Bem, o modelo vê tudo no texto, vamos simplesmente listar no prompt qual é o orçamento, quais produtos e o que o usuário escolheu”. Essa abordagem rapidamente bate nos limites de contexto e não oferece nenhuma garantia de integridade: o modelo pode “esquecer” um fato importante ou confundir identificadores. Coisas críticas de negócio (dinheiro, reservas, pedidos) devem viver no seu backend/MCP como fonte da verdade.
Erro nº 2: tentar construir todo o workflow apenas com widgetState.
O widgetState no Apps SDK resolve o problema de sobrevivência do estado de UI entre desmontagens e remontagens do widget, e não o de armazenamento de longo prazo do workflow. Se tentar armazenar nele o perfil, o carrinho e o histórico de etapas, você terá caos ao trocar de dispositivo e a impossibilidade de se restaurar após um longo tempo. O widget responde por detalhes visuais e conforto local. Toda a lógica do roteiro deve viver no servidor.
Erro nº 3: ausência de um workflowId explícito ou outra chave.
Acontece de o desenvolvedor confiar em identificadores implícitos como conversation_id, mas não introduzir seu próprio conceito de workflow. O resultado é que fica impossível distinguir um roteiro de outro, separar vários workflows paralelos ou restaurar exatamente o que é necessário. Uma simples string workflowId em todos os lugares onde há ferramentas e endpoints de API resolve muitos problemas, especialmente no MCP, que é stateless por protocolo.
Erro nº 4: misturar estado de UI e lógica de negócio.
Situação clássica: no widgetState colocam não só “qual aba está aberta”, mas também “quais produtos estão no carrinho”, e depois tentam tomar decisões no servidor com base nesse estado. No fim, com a menor dessincronização (o widget renderizou, mas a requisição ainda não chegou, ou vice-versa), o modelo vê uma realidade, a UI outra e o banco uma terceira. A fronteira de responsabilidade deve ser clara: o servidor armazena e valida os dados de negócio, o widget os exibe e dá ao usuário uma forma conveniente de alterá-los.
Erro nº 5: ausência de um roteiro de restauração e rollback.
É muito fácil desenhar um “caminho feliz” bonito, no qual o usuário segue perfeitamente as etapas, nada quebra, o ChatGPT não recarrega e a conexão não cai. Na realidade, cada etapa pode falhar, o usuário pode sair no meio e voltar uma semana depois. Se você não modelou a estrutura do WorkflowContext, não pensou em como buscar o workflow “ativo” e não previu os botões “Voltar” e “Continuar depois”, seu roteiro será frágil e frustrante para os usuários. Um contexto bem projetado é a base para a tolerância a falhas, tema da próxima aula.
GO TO FULL VERSION