1. Qué es el contexto del workflow y por qué lo necesitas
En una aplicación web típica tienes bastante claro dónde vive el estado: base de datos, caché, más algo en el front como Redux o el estado local de React. En una ChatGPT App todo es más divertido: el estado se distribuye en tres mundos a la vez — dentro del modelo (historia del diálogo), dentro del widget (estado de UI) y en tu servidor/MCP (datos de negocio).
Por contexto del workflow entenderemos todo el conjunto de datos necesario para responder a las preguntas «en qué paso estamos» y «qué se sabe ya». Si hablamos de nuestro proyecto didáctico GiftGenius, el contexto incluye:
- perfil del destinatario del regalo: edad, sexo, intereses;
- presupuesto y, posiblemente, la moneda;
- lista de ideas generadas y cuáles de ellas el usuario ha marcado con like o ha ocultado;
- aspectos técnicos: identificador de sesión o workflow, estado («profile_collected», «ideas_shown», «checkout_started»).
Este contexto no solo te hace falta a ti como desarrollador backend. También lo necesita el propio modelo para entender qué preguntas ya se han hecho, qué herramientas ya se han invocado y de qué va la conversación ahora. Y le hace falta al usuario para no tener que empezar de cero al volver al chat.
El usuario intuitivamente piensa que «ChatGPT lo recuerda todo». En realidad, el modelo recuerda solo el texto del diálogo, y mientras quepa en la ventana de contexto. Las cosas estructuradas como order_id, cart_id o «lista de ideas con like» hay que guardarlas en tu servidor; de lo contrario tendrás una máquina perfecta para generar afirmaciones seguras, pero incorrectas.
2. Tres niveles de estado: UI, LLM, negocio
Lo más cómodo es entender la persistencia del contexto a través del modelo de tres capas de estado, la «State Triad».
Tabla de niveles
Usemos una tabla pequeña:
| Nivel | Dónde reside | Ciclo de vida | De qué se encarga | Ejemplo en GiftGenius |
|---|---|---|---|---|
| UI State | Widget (React, widgetState) | Mientras el chat/mensaje con el widget esté abierto | Estado visual, entrada local | Qué tarjetas están resaltadas, estado del formulario |
| LLM Context | Historial del chat en OpenAI | Mientras quepa en la ventana de contexto | Comprensión del diálogo y razonamiento | «Buscamos un regalo para mamá, presupuesto $50» |
| Business State | MCP / tu backend (DB/Redis) | Todo lo que quieras (persistente) | Fuente de verdad: datos verificados, estados | { step: "ideas", budget: 50, liked: [42, 51] } |
La capa de UI es rápida y reactiva, pero muy frágil: ChatGPT puede «desmontar» el iframe con el widget cuando te desplazas hacia arriba en el historial, y luego montarlo de nuevo. Precisamente para eso existe widgetState, que vive un poco más que el componente de React y se sincroniza con el cliente host de ChatGPT.
La capa LLM da al modelo la sensación de diálogo continuo, pero solo guarda texto e invocaciones de tools. Puedes poner ahí un JSON con tu carrito, pero en esencia será insertar JSON en texto: el modelo no lo tratará como una base de datos.
La capa de negocio es lo que tú, como ingeniero, puedes controlar: ahí están los datos validados, índices, estados de pedidos. En cuanto tengas un flujo serio (regalos, reservas, aprendizaje), esta capa debe convertirse en la fuente principal de verdad sobre el estado.
El principal problema de ingeniería es evitar que estas tres capas se desalineen. El usuario cambia el presupuesto en el widget, el modelo sigue pensando en el anterior y en la base hay un tercer valor: receta clásica de comportamientos extraños.
3. Qué guardamos exactamente: estructura de WorkflowContext
Para hablar con concreción, describamos en TypeScript la interfaz de contexto para GiftGenius. Supongamos que ya tenemos varios pasos: recopilación de perfil, selección de presupuesto, generación de ideas y vista/likes.
Empecemos con una estructura sencilla:
// backend/types/workflow.ts
export type GiftWorkflowStep =
| "profile"
| "budget"
| "ideas"
| "checkout";
export interface GiftWorkflowContext {
id: string; // workflowId — identificador del escenario
userId?: string; // si ya hay autenticación 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/limpieza
}
No es el esquema definitivo, pero los elementos importantes ya están. Hay:
- un identificador de workflow, por el que buscaremos este contexto;
- el paso actual, que ayudará tanto al widget como al modelo a entender hasta dónde hemos llegado;
- un conjunto de campos que se irán completando en pasos separados;
- campos de servicio como la hora de actualización.
Una nota aparte sobre identificadores. En esta lección por workflowId entendemos el identificador de un escenario concreto dentro de nuestro backend/MCP. Puede coincidir con el identificador de sesión del diálogo de ChatGPT (sessionId), pero no dependemos de ello. userId es el identificador del usuario en tu sistema de autenticación (si lo hay); un usuario puede tener varios workflows activos. En el campo id está precisamente ese workflowId, con el que buscamos y actualizamos el contexto.
En las siguientes secciones veremos tres cosas: dónde guardar estos objetos, cómo escribirlos allí y cómo recuperarlos — tanto en el widget como en el modelo.
4. Dónde guardar el estado: opciones y compromisos
Es útil pensar en la persistencia del estado en dos planos: dónde se guarda y cuánto dura. En esta sección nos centraremos en el lugar de almacenamiento, y el tiempo de vida lo retomaremos en el checklist y el bloque de errores típicos.
Primero, veamos el lugar de almacenamiento.
Dentro del diálogo (en el prompt)
A veces apetece decir: «Devolvamos cada vez a la modelo un JSON con el estado actual y que lo gestione ella». Eso funciona para escenarios muy simples y cadenas de pocos pasos, pero tropieza rápidamente con dos problemas: la longitud limitada del contexto y la ausencia de garantías de integridad de datos.
Además, el protocolo MCP por naturaleza es stateless: como HTTP, no guarda estado entre solicitudes por defecto. Para vincular una invocación de tool a una sesión concreta, debes pasar explícitamente un identificador — workflow o session id — ya sea en los argumentos de la herramienta o mediante metadatos/cabeceras.
Por eso, guardar el estado de negocio solo en el diálogo es más un experimento didáctico que una arquitectura.
En el widget: UI + widgetState
En el nivel de UI usamos el estado típico de React (useState, useReducer, etc.), pero, como ya se ha dicho, el componente puede desmontarse. En Apps SDK existe para esto el mecanismo widgetState, que vive fuera de React y se sincroniza con el host de ChatGPT. Si al montar el widget recuperas de ahí el valor guardado y, cuando cambia, lo vuelves a guardar, obtienes un almacén local bastante cómodo.
Este almacén es ideal para estado puramente visual: qué tarjetas están plegadas, en qué pestaña estás, qué ha escrito el usuario en un formulario antes de pulsar «Siguiente». Pero no sustituye al servidor: en cuanto el usuario abra el chat en otro dispositivo o dentro de una semana, widgetState puede no servir. Y basar en él la lógica de negocio es cuestionable.
En el servidor/MCP: Map, Redis, BD
Finalmente, la opción principal para producción: guardamos GiftWorkflowContext en el lado del servidor MCP o en un servicio backend. Como el cliente y el servidor MCP son stateless por protocolo, debemos pasar el workflowId (o state_token) en cada llamada de herramienta para saber qué contexto actualizar.
Hay varias opciones de implementación:
- Map en memoria en Node.js — sirve para demos y entornos de desarrollo: todo es rápido, pero desaparece al reiniciar;
- Redis u otro caché en memoria con TTL — bueno para wizards de pocos pasos: vive una o dos horas y luego se puede eliminar;
- una base de datos SQL/NoSQL normal — imprescindible para escenarios del tipo «vuelvo dentro de una semana» o «borradores y carritos».
En esta lección no profundizaremos en una BD concreta; nos centraremos en la interfaz y en qué debe entrar ahí.
5. Almacenamiento más simple en el servidor MCP: Map por workflowId
Empecemos con algo aterrizado: una Map en memoria en el servidor MCP, donde la clave es el workflowId. En la demo educativa se puede igualar al sessionId del diálogo, pero en prod es mejor mantener el workflowId como identificador independiente del escenario. El valor en esta Map será GiftWorkflowContext. En producción real lo cambiarás por Redis o una BD, pero el API seguirá siendo el mismo.
Supongamos que tenemos un servidor MCP en TypeScript. Añadimos algo cerca de la inicialización:
// 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() });
}
Después — una herramienta que guarda el perfil del destinatario. Es importante que reciba el workflowId y los datos del perfil, y que dentro actualice/cree el contexto correspondiente:
// mcp/tools/setProfile.ts
import { jsonSchema } from "@modelcontextprotocol/sdk"; // alias
import { getWorkflow, saveWorkflow } from "../workflowStore";
export const setProfileTool = {
name: "gift_set_profile",
description: "Guarda el perfil del destinatario del regalo",
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
}
};
}
};
Esta herramienta ya resuelve dos tareas: guarda el perfil y avanza currentStep al siguiente paso. En un proyecto real quizá quieras separar las herramientas «guardar datos» y «avanzar al paso», pero para entender el concepto este enfoque sirve.
Presta atención al workflowId en los argumentos: es este parámetro el que vincula la invocación de la herramienta con el contexto adecuado. La parte cliente (widget o agente) debe guardarlo en algún sitio y pasarlo.
6. Integración con Apps SDK: dónde obtener workflowId y sessionId
La pregunta «de dónde obtener el workflowId» en ChatGPT Apps es un poco filosófica. Las posibilidades dependen de si usas autenticación, MCP directamente o Agents SDK. En líneas generales, las opciones son: generarlo en el servidor en la primera invocación de tool o generarlo en el widget y pasarlo hacia abajo.
Para el ejemplo didáctico, supongamos que el primer paso es una llamada a una herramienta MCP que crea el workflow, y el widget simplemente recoge después su id.
La variante más simple:
// mcp/tools/startWorkflow.ts
import { randomUUID } from "crypto";
import { saveWorkflow } from "../workflowStore";
export const startWorkflowTool = {
name: "gift_start_workflow",
description: "Crea un nuevo workflow de selección de regalo",
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"
}
};
}
};
Después, al recibir el workflowId en la respuesta de la herramienta, el modelo puede:
- mantenerlo de forma oculta en el contexto;
- pasarlo al widget mediante structuredContent, para que el widget lo guarde en widgetState y lo inserte en las siguientes invocaciones de herramientas.
En el lado del widget el código será aproximadamente así.
7. Guardar workflowId y estado de UI local en el widget
Supongamos que tenemos un widget de lista de ideas que quiere saber qué workflow muestra y recordar los likes locales incluso si el componente se desmonta. En versión simplificada:
// app/widgets/GiftIdeasWidget.tsx
import { useEffect, useState } from "react";
interface Idea {
id: string;
title: string;
}
interface WidgetProps {
widgetId: string;
workflowId: string; // llegó desde 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);
// aquí también se puede invocar el 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>
);
}
Aquí widgetState se usa como capa de UI: recordamos qué ideas están resaltadas. Lo correcto sería además enviar los likes al servidor (mediante una herramienta MCP o un endpoint API en Next.js) para que la capa de negocio sepa también lo que el usuario eligió.
Es importante no intentar construir todo el workflow sobre widgetState. Debe ser una capa adicional al contexto de negocio en el servidor.
8. Reanudar el flujo: el usuario ha vuelto
Pasemos ahora a un caso más interesante: el usuario cerró ChatGPT, volvió al cabo de unas horas o días y abrió de nuevo el mismo chat. ¿Qué debería pasar?
El UX ideal sería: el modelo y la App entienden que el usuario ya tiene un workflow sin terminar, recuperan su contexto y dicen algo como: «Ya indicaste el perfil y el presupuesto; sigamos con la selección de ideas».
Arquitectónicamente se ve así:
- En tu servidor se guarda un GiftWorkflowContext, vinculado a algún userId o, al menos, a un workflowId interno.
- Ante una nueva solicitud (o la primera invocación de tool dentro del diálogo) la App consulta al servidor: «¿Hay un workflow activo para este usuario?».
- Si lo hay, el servidor lo devuelve y, quizá, una marca especial resume que el modelo usará en su respuesta.
En una demo monolítica sencilla puedes asumir que el servidor MCP y la aplicación Next.js viven en un mismo repositorio (o incluso proceso), así que simplemente reutilizamos el mismo workflowStore de MCP en las rutas API.
En Next.js esto puede ser una ruta API simple:
// app/api/gift/workflow/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getWorkflow } from "@/mcp/workflowStore"; // en esta demo MCP y Next.js comparten el mismo almacenamiento
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
});
}
El widget (o una herramienta MCP) puede llamar a este endpoint cuando necesite actualizar el estado: por ejemplo, al primer montaje o al cambiar de paso. En la configuración didáctica basta con la combinación workflowId + almacenamiento en Map; en producción real añadirás autorización y verificación de pertenencia al usuario.
Si usas Agents SDK u otra orquestación más compleja, la idea se puede extender a «checkpoints»: guardar el estado al final de pasos grandes, desde donde el agente puede continuar al reiniciarse. Pero eso ya es materia del siguiente módulo.
9. Avance/retroceso y el historial de pasos
Inevitablemente surge la pregunta: «¿Se puede volver un paso atrás?» Para el usuario es un deseo muy natural: cambiar el presupuesto, ajustar intereses, quitar un artículo de la selección.
Técnicamente esto implica dos cosas:
- hay que guardar no solo el paso actual, sino también el historial de decisiones;
- hay que recalcular con cuidado los datos derivados después de un rollback.
Una opción es añadir al contexto un campo history que contenga instantáneas de pasos. Por ejemplo:
export interface StepSnapshot {
step: GiftWorkflowStep;
payload: any; // datos concretos del paso
createdAt: number;
}
export interface GiftWorkflowContext {
// ...campos anteriores
history: StepSnapshot[];
}
Cuando el usuario rellena el perfil, añades al historial una instantánea con step: "profile". Cuando cambia el presupuesto, otra instantánea. Al retroceder al perfil:
- actualizas currentStep = "profile";
- opcionalmente recortas el historial hasta el índice necesario;
- recalculas los valores derivados (por ejemplo, limpias ideas y likes si dependen del presupuesto).
A nivel de modelo, hay que sincronizarse: si el usuario pulsa en el widget el botón «Atrás», hay que enviar una invocación de tool que actualice el contexto de negocio y devuelva en la respuesta una descripción explícita del nuevo estado. Si no, tendrás la clásica desincronización: el UI muestra el paso 2 y el modelo cree que estás en el 3.
En el nivel del widget, el retroceso puede verse como un botón sencillo:
async function goBackToProfile() {
await fetch("/api/gift/workflow/back", {
method: "POST",
body: JSON.stringify({ workflowId, targetStep: "profile" })
});
// actualizamos la UI, limpiamos el estado local
}
Y será el servidor quien decida qué limpiar en el contexto y qué mensaje enviar al modelo mediante la respuesta de la herramienta.
10. Cómo conectar todo esto con el modelo: contexto para el razonamiento
Todo lo que hacemos con el estado, al final, le hace falta no solo al usuario, sino también a la LLM. El modelo debe entender:
- qué se sabe ya (por ejemplo, el perfil del destinatario y el presupuesto);
- qué pasos ya se han completado;
- si hay procesos sin terminar.
La forma de entregar esta información al modelo depende de la arquitectura de la App: puedes inyectarla en el system prompt, devolverla en ToolOutput en forma estructurada o usar campos especiales _meta/anotaciones, si el SDK los soporta.
El patrón típico es:
- La herramienta MCP devuelve en structuredContent una instantánea breve del contexto: paso actual, campos clave y, quizá, workflowId.
- Apps SDK lo transforma en un widget o en texto + datos ocultos.
- El modelo, al ver el structuredContent, entiende que el flujo continúa y planifica la siguiente acción en base a ello.
En algunos casos, si el modelo «olvida» parámetros importantes o empieza a alucinar, puedes actualizar el contexto de forma forzada: invocar una herramienta especial que devuelva el estado actual y el modelo «volverá al contexto».
Es importante no intentar meter en el modelo todo el GiftWorkflowContext hasta el último campo. Basta con lo clave: para quién buscamos el regalo, qué presupuesto, cuántas ideas se han mostrado ya, si hay un checkout sin terminar.
11. Mini checklist para diseñar el WorkflowContext
Antes de pasar a los errores típicos, conviene formular un conjunto breve de preguntas a las que debes responderte cuando diseñes el contexto del workflow (puedes escribirlo literalmente junto a la interfaz):
- ¿Qué pasos tiene el escenario y qué conjunto mínimo de datos necesita cada uno?
Esto te protegerá de monstruos JSON gigantes «por si acaso». - ¿Qué hay que recordar solo dentro de un chat y qué — entre sesiones y dispositivos?
Lo primero se puede dejar en widgetState y en los prompts; lo segundo debe ir obligatoriamente a la BD del servidor. - ¿Cómo será el identificador del contexto?
Puede ser la combinación userId + scenario, un workflowId independiente, o ambas cosas. Lo principal es poder encontrar inequívocamente el contexto en la base. - ¿Cómo limpiarás los workflows antiguos?
En una demo vale «no limpiar nunca», pero en prod necesitarás TTL o jobs en segundo plano que eliminen workflows antiguos. - ¿Necesita el usuario retroceder y cómo lo implementarás?
Si guardarás un árbol de ramas o basta con una lista lineal de pasos con posibilidad de rollback.
Y por último: intenta imaginar el escenario «el usuario vuelve dentro de una semana en otro chat». Si no puedes explicar cómo sabrá la App del workflow antiguo y qué debe mostrar, hay que reforzar la parte de almacenamiento persistente.
12. Errores típicos al trabajar con el contexto entre pasos
Error n.º 1: guardarlo todo solo en el historial del diálogo.
A veces surge la tentación: «Como el modelo ve todo en el texto, enumeremos cada vez en el prompt el presupuesto, los artículos y lo que eligió el usuario». Este enfoque choca pronto con los límites del contexto y no da ninguna garantía de integridad: el modelo puede «olvidar» un hecho importante o confundir identificadores. Las cosas críticas de negocio (dinero, reservas, pedidos) deben vivir en tu backend/MCP como fuente de verdad.
Error n.º 2: intentar construir todo el workflow solo sobre widgetState.
widgetState en Apps SDK resuelve la supervivencia del estado de UI entre el desmontaje y el montaje del widget, no el almacenamiento a largo plazo del workflow. Si intentas guardar ahí el perfil, el carrito y el historial de pasos, obtendrás caos al cambiar de dispositivo e imposibilidad de recuperarte tras mucho tiempo. El widget se encarga de los detalles visuales y la comodidad local. Toda la lógica del flujo debe vivir en el servidor.
Error n.º 3: ausencia de un workflowId explícito u otra clave.
A veces el desarrollador confía en identificadores implícitos como conversation_id, pero no introduce su propia noción de workflow. Como resultado, se vuelve imposible distinguir un escenario de otro, separar varios workflows en paralelo o recuperar justo el que hace falta. Una simple cadena workflowId en todos los lugares donde haya herramientas y endpoints API resuelve muchos problemas, especialmente en MCP, que por protocolo es stateless.
Error n.º 4: mezclar estado de UI y lógica de negocio.
Situación clásica: en widgetState se guarda no solo «qué pestaña está abierta», sino también «qué artículos hay en el carrito», y luego se intenta tomar decisiones en el servidor basándose en ese estado. Al mínimo desajuste (el widget se renderizó pero la petición aún no llegó, o al revés), el modelo ve una realidad, el UI otra y la base una tercera. La frontera de responsabilidades debe ser clara: el servidor guarda y valida los datos de negocio; el widget los muestra y ofrece al usuario una forma cómoda de cambiarlos.
Error n.º 5: ausencia de un plan de reanudación y retroceso.
Es muy fácil dibujar un «camino feliz» precioso, donde el usuario avanza perfecto por los pasos, no falla nada, ChatGPT no se reinicia y el túnel no se corta. En la realidad, cada paso puede fallar, el usuario puede irse a mitad y volver en una semana. Si no has diseñado la estructura de WorkflowContext, no has pensado cómo buscar el workflow «activo» y no has previsto los botones «Atrás» y «Continuar más tarde», tu flujo será frágil y frustrante para los usuarios. Un contexto bien pensado es la base de la tolerancia a fallos, de la que hablaremos en la siguiente lección.
GO TO FULL VERSION