1. Che cos’è il contesto del workflow e perché serve
In una normale applicazione web avete un’idea abbastanza chiara di dove viva lo stato: database, cache, più qualcosa sul front come Redux o il local state di React. In una ChatGPT App è più divertente: lo stato è sparso subito su tre mondi — dentro il modello (cronologia della conversazione), dentro il widget (UI state) e sul vostro server/MCP (dati di business).
Per contesto del workflow intendiamo l’insieme dei dati necessari per rispondere alle domande «a che passo siamo» e «che cosa è già noto». Prendendo il nostro GiftGenius didattico, il contesto include:
- profilo del destinatario del regalo: età, sesso, interessi;
- budget e, se necessario, valuta;
- elenco delle idee generate e quali di esse l’utente ha messo Mi piace o ha nascosto;
- aspetti tecnici: identificatore di sessione o workflow, stato («profile_collected», «ideas_shown», «checkout_started»).
Questo contesto serve non solo a voi come sviluppatori backend. Serve al modello stesso per capire quali domande sono già state poste, quali strumenti sono già stati invocati e di che cosa si stia parlando. E serve all’utente per non dover ricominciare tutto da zero quando ritorna nella chat.
L’utente pensa intuitivamente che «ChatGPT ricorda tutto». In realtà il modello ricorda solo il testo della conversazione, finché rientra nella finestra di contesto. Cose strutturate come order_id, cart_id o «elenco delle idee con Mi piace» vanno conservate sul vostro server, altrimenti otterrete una macchina perfetta nel generare affermazioni sicure ma sbagliate.
2. Tre livelli di stato: UI, LLM, business
Conviene comprendere la conservazione del contesto attraverso un modello a tre strati di stato. La cosiddetta «State Triad».
Tabella dei livelli
Usiamo una piccola tabella:
| Livello | Dove vive | Ciclo di vita | Di cosa si occupa | Esempio in GiftGenius |
|---|---|---|---|---|
| UI State | Widget (React, widgetState) | Finché la chat/messaggio con il widget è aperto | Stato visivo, input locale | Quali card sono evidenziate, stato del form |
| LLM Context | Cronologia chat in OpenAI | Finché il messaggio «entra» nel contesto | Comprensione del dialogo e ragionamento | «Stiamo cercando un regalo per la mamma, budget $50» |
| Business State | MCP / vostro backend (DB/Redis) | Quanto volete (persistente) | Fonte di verità: dati verificati, stati | { step: "ideas", budget: 50, liked: [42, 51] } |
Lo strato UI è veloce e reattivo, ma molto fragile: ChatGPT può «smontare» l’iframe con il widget quando scorrete verso l’alto nella cronologia e poi montarlo di nuovo. Proprio per questo esiste widgetState, che vive un po’ più a lungo del componente React e si sincronizza con il client host di ChatGPT.
Lo strato LLM dà al modello la sensazione di dialogo continuo, ma conserva solo testo e invocazioni degli strumenti (tool). Potete metterci dentro un JSON con il vostro carrello, ma in sostanza è solo inserire JSON nel testo — il modello non lo tratterà come un database.
Lo strato business è ciò che potete controllare da ingegneri: lì stanno i dati validati, gli indici, gli stati degli ordini. Non appena avete uno scenario serio (regali, prenotazioni, formazione), questo strato deve diventare la principale fonte di verità sullo stato.
Il problema ingegneristico principale è evitare che questi tre strati divergano. L’utente nel widget cambia il budget, il modello pensa ancora a quello vecchio e nel database ce n’è un terzo valore: è la ricetta classica per comportamenti strani.
3. Cosa salviamo esattamente: la struttura di WorkflowContext
Per parlare in modo concreto, descriviamo in TypeScript un’interfaccia di contesto per GiftGenius. Supponiamo di avere già alcuni passaggi: raccolta del profilo, scelta del budget, generazione delle idee e visualizzazione/Mi piace.
Cominciamo con una struttura semplice:
// backend/types/workflow.ts
export type GiftWorkflowStep =
| "profile"
| "budget"
| "ideas"
| "checkout";
export interface GiftWorkflowContext {
id: string; // workflowId — identificatore dello scenario
userId?: string; // se l'autenticazione è già configurata
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 per TTL/pulizia
}
Non è lo schema finale, ma gli elementi importanti ci sono già. Abbiamo:
- un identificatore di workflow con cui cercheremo questo contesto;
- il passo corrente, che aiuterà sia il widget sia il modello a capire fino a dove siamo arrivati;
- un insieme di campi che verranno compilati nei singoli passaggi;
- campi di servizio come l’ora dell’ultimo aggiornamento.
Una nota a parte sugli identificatori. In questa lezione per workflowId intendiamo l’identificatore di uno specifico scenario all’interno del nostro backend/MCP. Può coincidere con l’identificatore della sessione di dialogo di ChatGPT (sessionId), ma non ci contiamo. userId è l’identificatore dell’utente nel vostro sistema di autenticazione (se presente); un utente può avere più workflow attivi. Nel campo id sta proprio questo workflowId, con cui cerchiamo e aggiorniamo il contesto.
Nei prossimi capitoli vedremo tre cose: dove conservare questi oggetti, come scriverli lì dentro e come recuperarli — sia nel widget sia nel modello.
4. Dove conservare lo stato: opzioni e compromessi
È comodo ragionare sulla conservazione dello stato in due dimensioni: dove si conserva e quanto vive. In questa sezione ci concentriamo sul luogo di conservazione e torneremo sui tempi di vita nella checklist e nel blocco sugli errori tipici.
Partiamo dal luogo di conservazione.
Dentro la conversazione (nel prompt)
A volte viene da dire: «Basta restituire ogni volta al modello un JSON con lo stato corrente e ci penserà lui». Funziona per scenari molto semplici e brevi catene di passaggi, ma si scontra rapidamente con due problemi: limite della lunghezza del contesto e assenza di qualsiasi garanzia di integrità dei dati.
Inoltre, il protocollo MCP è per sua natura stateless: come HTTP, non conserva alcuno stato tra le richieste per impostazione predefinita. Per collegare una chiamata a un tool a una sessione specifica, dovete passare esplicitamente un identificatore — workflow o session id — o negli argomenti dello strumento, o tramite metadati/intestazioni.
Perciò conservare lo stato di business solo nella conversazione è più un esperimento didattico che un’architettura.
Nel widget: UI + widgetState
A livello UI usiamo il normale state di React (useState, useReducer e così via), ma, come già detto, il componente può essere smontato. Nell’Apps SDK c’è il meccanismo widgetState, che vive fuori da React e si sincronizza con l’host di ChatGPT. Se al montaggio del widget ne estraete il valore salvato e, in caso di modifiche, lo rimettete dentro, ottenete un archivio locale abbastanza comodo.
Questo archivio è perfetto per lo stato puramente visivo: quali card sono attualmente compresse, in quale tab vi trovate, che cosa ha inserito l’utente nel form prima di premere «Avanti». Ma non sostituisce il server: non appena l’utente apre la chat su un altro dispositivo o dopo una settimana, widgetState potrebbe non bastare. E costruirci sopra la logica di business è discutibile.
Sul server/MCP: Map, Redis, DB
Infine, l’opzione principale per la produzione: conserviamo GiftWorkflowContext lato server MCP o in un servizio backend. Poiché client e server MCP sono stateless per protocollo, dobbiamo propagare il workflowId (o state_token) in ogni invocazione dello strumento, per capire quale contesto aggiornare.
Le opzioni implementative sono diverse:
- Map in‑memory in Node.js — adatta per demo e ambienti di sviluppo: tutto è veloce ma scompare al riavvio;
- Redis o un’altra cache in‑memory con TTL — ideale per wizard di pochi passaggi: vive un’ora o due e poi si può eliminare;
- un normale database SQL/NoSQL — obbligatorio per scenari tipo «torno dopo una settimana» o «bozze e carrelli».
In questa lezione non approfondiremo un DB specifico; ci concentreremo sull’interfaccia e su cosa deve finirci dentro.
5. Storage più semplice sul server MCP: Map per workflowId
Partiamo da qualcosa di concreto: una Map in‑memory sul server MCP, dove la chiave è il workflowId. In una demo didattica lo si può semplicemente eguagliare al sessionId della conversazione, ma in produzione è meglio tenere workflowId come identificatore separato dello scenario. Il valore in questa Map sarà GiftWorkflowContext. In produzione reale lo sostituirete con Redis o un DB, ma l’API resterà la stessa.
Supponiamo di avere un server MCP in TypeScript. Aggiungiamo vicino all’inizializzazione:
// 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() });
}
Poi — uno strumento che salva il profilo del destinatario. Importante che riceva workflowId e i dati del profilo e che all’interno aggiorni/crei il contesto corrispondente:
// mcp/tools/setProfile.ts
import { jsonSchema } from "@modelcontextprotocol/sdk"; // alias
import { getWorkflow, saveWorkflow } from "../workflowStore";
export const setProfileTool = {
name: "gift_set_profile",
description: "Salva il profilo 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
}
};
}
};
Questo strumento risolve già due compiti: salva il profilo e sposta currentStep al passo successivo. In un progetto reale potreste voler separare gli strumenti «salva i dati» e «vai al passo», ma per comprendere il concetto va bene così.
Fate attenzione al workflowId negli argomenti: è proprio questo parametro che lega l’invocazione del tool al contesto giusto. La parte client (widget o agente) deve conservarlo da qualche parte e propagarlo.
6. Integrazione con Apps SDK: dove ottenere workflowId e sessionId
La domanda «da dove prendere il workflowId» nelle ChatGPT Apps è un po’ filosofica. Le possibilità dipendono dal fatto che usiate o meno autenticazione, MCP diretto o Agents SDK. In generale, le opzioni sono: generazione lato server alla prima invocazione di un tool oppure generazione nel widget e passaggio verso il basso.
Per l’esempio didattico supponiamo che il primo passo sia l’invocazione di uno strumento MCP che crea il workflow e che il widget poi recuperi solo il suo id.
La variante più semplice:
// mcp/tools/startWorkflow.ts
import { randomUUID } from "crypto";
import { saveWorkflow } from "../workflowStore";
export const startWorkflowTool = {
name: "gift_start_workflow",
description: "Crea un nuovo workflow per la selezione del 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"
}
};
}
};
Poi il modello, avendo ricevuto il workflowId nella risposta dello strumento, può:
- tenerlo in forma nascosta nel contesto;
- passarlo al widget tramite structuredContent, in modo che il widget lo salvi in widgetState e lo inserisca nelle successive invocazioni degli strumenti.
Dal lato widget, il codice sarà più o meno così.
7. Conservare workflowId e stato UI locale nel widget
Supponiamo di avere un widget con la lista delle idee, che vuole sapere quale workflow sta mostrando e ricordare i Mi piace locali anche se il componente viene smontato. In forma semplificata:
// app/widgets/GiftIdeasWidget.tsx
import { useEffect, useState } from "react";
interface Idea {
id: string;
title: string;
}
interface WidgetProps {
widgetId: string;
workflowId: string; // arrivato da 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);
// qui si può anche chiamare il tool MCP "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>
);
}
Qui widgetState è usato proprio come strato UI: ricordiamo quali idee sono evidenziate. In teoria i Mi piace andrebbero inviati anche al server (tramite un tool MCP o un endpoint API in Next.js), così che anche lo strato di business sappia che cosa ha scelto l’utente.
È importante non cercare di costruire l’intero workflow su widgetState. Deve essere uno strato aggiuntivo al contesto di business sul server.
8. Ripristino dello scenario: l’utente è tornato
Passiamo ora a un caso più interessante: l’utente ha chiuso ChatGPT, torna dopo qualche ora o giorno e riapre la stessa chat. Cosa dovrebbe succedere?
La UX ideale è questa: il modello e l’App capiscono che l’utente ha già un workflow non completato, ne recuperano il contesto e dicono qualcosa tipo: «Avete già indicato profilo e budget, continuiamo con la scelta delle idee».
Architettonicamente appare così:
- Sul vostro server è conservato un GiftWorkflowContext collegato a un certo userId o almeno al workflowId interno.
- Alla nuova richiesta (o alla prima invocazione di un tool nel dialogo) l’App chiama il server chiedendo: «Esiste un workflow attivo per questo utente?».
- Se esiste, il server lo restituisce e, eventualmente, un flag speciale resume che il modello usa nella sua risposta.
In una semplice demo monolitica si può considerare che server MCP e applicazione Next.js vivano nello stesso repository (o addirittura nello stesso processo), quindi riutilizziamo lo stesso workflowStore sia in MCP sia nelle API routes.
In Next.js può essere una semplice API route:
// app/api/gift/workflow/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getWorkflow } from "@/mcp/workflowStore"; // in questa demo MCP e Next.js condividono lo stesso archivio
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
});
}
Il widget (o un tool MCP) può chiamare questo endpoint quando deve aggiornare lo stato: ad esempio al primo montaggio o quando si cambia passo. Nella configurazione didattica basta la coppia workflowId + storage in Map; in produzione reale aggiungerete autorizzazione e verifica di appartenenza all’utente.
Se usate l’Agents SDK o un’orchestrazione più complessa, potete estendere l’idea a «checkpoint» — salvare lo stato alla fine di grandi passaggi, da cui l’agente può riprendere al riavvio. Ma questo è l’argomento del prossimo modulo.
9. Navigazione avanti‑indietro e cronologia dei passaggi
Sorge inevitabilmente la domanda: «Si può tornare a un passaggio precedente?» Per l’utente è un desiderio molto naturale: modificare il budget, correggere gli interessi, togliere un articolo dalla selezione.
Tecnicamente significa due cose:
- bisogna conservare non solo il passo corrente ma anche la cronologia delle decisioni prese;
- bisogna ricalcolare con cura i dati derivati dopo il rollback.
Una variante è aggiungere nel contesto un campo history che contenga le istantanee dei passaggi. Per esempio:
export interface StepSnapshot {
step: GiftWorkflowStep;
payload: any; // dati specifici del passaggio
createdAt: number;
}
export interface GiftWorkflowContext {
// ...campi precedenti
history: StepSnapshot[];
}
Quando l’utente compila il profilo, aggiungete alla cronologia un’istantanea con step: "profile". Quando modifica il budget — un’altra istantanea. In caso di rollback al profilo:
- aggiornate currentStep = "profile";
- facoltativamente troncherete la cronologia all’indice necessario;
- ricalcolerete i valori derivati (ad esempio, svuotate idee e Mi piace se dipendono dal budget).
A livello di modello è importante sincronizzarsi: se l’utente ha premuto nel widget il pulsante «Indietro», dovete inviare un’invocazione di tool che aggiorni il contesto di business e restituisca nella risposta una descrizione esplicita del nuovo stato. Altrimenti avrete il classico problema di desincronizzazione: la UI mostra il passo 2, ma il modello è convinto che siate al passo 3.
Nel widget il rollback può essere un semplice pulsante:
async function goBackToProfile() {
await fetch("/api/gift/workflow/back", {
method: "POST",
body: JSON.stringify({ workflowId, targetStep: "profile" })
});
// aggiorniamo la UI, puliamo lo stato locale
}
Sarà poi il server a decidere che cosa pulire nel contesto e quale messaggio inviare al modello tramite la risposta del tool.
10. Come collegare tutto questo al modello: contesto per il ragionamento
Tutto ciò che facciamo con lo stato serve alla fine non solo all’utente, ma anche all’LLM. Il modello deve capire:
- che cosa è già noto (ad esempio, il profilo del destinatario e il budget);
- quali passaggi sono già stati completati;
- se ci sono processi non conclusi.
Il modo di fornire queste informazioni al modello dipende dall’architettura dell’App: potete iniettarle nel system prompt, restituirle nel ToolOutput in forma strutturata o usare campi speciali _meta/annotations, se supportati dall’SDK.
Il pattern tipico è questo:
- Il tool MCP restituisce in structuredContent una breve istantanea del contesto: passo corrente, campi chiave e, se serve, workflowId.
- L’Apps SDK lo trasforma in un widget o in testo + dati nascosti.
- Il modello, vedendo lo structuredContent, capisce che lo scenario è proseguito e costruisce l’azione successiva di conseguenza.
In alcuni casi, se il modello «ha dimenticato» parametri importanti o ha iniziato ad allucinare, potete aggiornare forzatamente il contesto: invocate un tool speciale che restituisca lo stato attuale e il modello «rientrerà nel contesto».
È importante non cercare di infilare nel modello l’intero GiftWorkflowContext fino all’ultimo campo. Bastano gli elementi chiave: per chi stiamo cercando il regalo, quale budget, quante idee sono già state mostrate, se c’è un checkout non concluso.
11. Mini checklist per progettare il WorkflowContext
Prima di passare agli errori tipici, è utile formulare un piccolo insieme di domande a cui dovete rispondere quando progettate il contesto del workflow (potete letteralmente annotarlo accanto all’interfaccia):
- Quali passaggi ha lo scenario e qual è il set minimo di dati necessario in ciascuno?
Questo vi proteggerà dai mostri JSON «per ogni evenienza». - Che cosa serve ricordare solo nell’ambito di una singola chat e che cosa tra sessioni e dispositivi?
Il primo può restare in widgetState e nei prompt, il secondo va inviato obbligatoriamente nel DB lato server. - Come sarà fatto l’identificatore del contesto?
Può essere la coppia userId + scenario, un workflowId separato o entrambi. L’importante è poter trovare in modo univoco il contesto nel database. - Come pulirete i workflow vecchi?
Per la demo si può «non pulire mai», ma in produzione vi serviranno o TTL o job in background che eliminano i workflow obsoleti. - All’utente serve il rollback e come lo implementerete?
Conserverete un albero di rami o basta una lista lineare di passaggi con possibilità di rollback.
Infine: provate a ripercorrere mentalmente lo scenario «l’utente è tornato dopo una settimana in un’altra chat». Se non sapete spiegare come l’App scoprirà il vecchio workflow e cosa mostrerà, dovete rafforzare la parte di conservazione persistente.
12. Errori tipici nella gestione del contesto tra i passaggi
Errore n. 1: conservare tutto solo nella cronologia della conversazione.
A volte c’è la tentazione: «Il modello vede tutto nel testo, elenchiamo ogni volta nel prompt budget, prodotti e scelte dell’utente». Questo approccio si scontra rapidamente con i limiti del contesto e non dà alcuna garanzia di integrità: il modello può tranquillamente «dimenticare» un fatto importante o confondere gli identificatori. Le cose critiche per il business (denaro, prenotazioni, ordini) devono vivere nel vostro backend/MCP come fonte di verità.
Errore n. 2: tentare di costruire l’intero workflow solo su widgetState.
widgetState nell’Apps SDK risolve il problema della sopravvivenza dello stato UI tra lo smontaggio e il rimontaggio del widget, non quello della conservazione a lungo termine del workflow. Se cercate di conservarci profilo, carrello e cronologia dei passaggi, otterrete caos al cambio dispositivo e impossibilità di ripristino dopo tempo. Il widget si occupa di dettagli visivi e comfort locale. Tutta la logica dello scenario deve vivere sul server.
Errore n. 3: assenza di un workflowId o di un’altra chiave esplicita.
Capita che lo sviluppatore si affidi a identificatori impliciti come conversation_id senza introdurre un proprio concetto di workflow. Diventa quindi impossibile distinguere uno scenario da un altro, separare più workflow paralleli o ripristinare proprio quello necessario. Una semplice stringa workflowId ovunque ci siano strumenti ed endpoint API risolve molti problemi, soprattutto in MCP, che per protocollo è stateless.
Errore n. 4: mescolare stato UI e logica di business.
Situazione classica: in widgetState si mettono non solo «quale tab è aperto», ma anche «quali prodotti sono nel carrello», e poi si cerca di prendere decisioni lato server basandosi su quello stato. Al minimo disallineamento (il widget si è renderizzato ma la richiesta non è ancora arrivata, o viceversa) il modello vede una realtà, la UI un’altra e il database una terza. Il confine delle responsabilità deve essere chiaro: il server conserva e valida i dati di business, il widget li mostra e offre all’utente un modo comodo per modificarli.
Errore n. 5: assenza di uno scenario di ripristino e rollback.
È facile disegnare un «percorso felice» dove l’utente procede perfettamente tra i passaggi, niente si rompe, ChatGPT non si ricarica e la connessione non si interrompe. Nella realtà ogni passaggio può fallire, l’utente può andarsene a metà e tornare dopo una settimana. Se non avete predisposto la struttura di WorkflowContext, non avete pensato a come trovare un workflow «attivo» e non avete previsto i pulsanti «Indietro» e «Continua più tardi», il vostro scenario sarà fragile e irritante per gli utenti. Un contesto ben progettato è la base della resilienza, di cui parleremo nella prossima lezione.
GO TO FULL VERSION