Actions : connecter votre backend
Mis à jour le 29 juillet 2026
Maîtriser les Actions du Apps SDK
Les Actions sont le cœur de toute application ChatGPT non triviale. Elles permettent au modèle d’exécuter du code côté serveur, d’interroger des API externes et de manipuler des données en temps réel. Vous en avez écrit une simple à la leçon précédente ; il s’agit maintenant d’en maîtriser tous les ressorts, car c’est là que se joue la fiabilité de votre app.
Anatomie d’une action
Une action se compose de quatre éléments, visibles dans l’exemple ci-dessous : son identité (nom et description), ses paramètres décrits par un schéma JSON, son handler qui porte la logique métier, et des métadonnées facultatives. Ces dernières méritent un mot — rateLimit protège votre backend contre un utilisateur qui relancerait la même recherche vingt fois de suite, et cacheTtl évite d’interroger votre base pour une requête identique servie il y a trente secondes.
import { defineAction } from "@openai/apps-sdk";
export const myAction = defineAction({
// 1. Identité
name: "searchProducts",
description: "Recherche des produits dans le catalogue par mot-clé et catégorie",
// 2. Paramètres (schema JSON)
parameters: {
query: {
type: "string",
description: "Terme de recherche",
required: true,
},
category: {
type: "string",
enum: ["electronics", "books", "clothing"],
description: "Catégorie de produits",
},
limit: {
type: "number",
description: "Nombre maximum de résultats",
default: 10,
},
},
// 3. Handler (logique métier)
handler: async ({ query, category, limit }) => {
const results = await db.products.search({ query, category, limit });
return { products: results, total: results.length };
},
// 4. Métadonnées (optionnel)
rateLimit: { maxCalls: 10, windowMs: 60000 },
cacheTtl: 300,
});
La description, c’est critique
Le modèle s’appuie sur la description pour décider quand appeler votre action. Une description vague produit mécaniquement des appels erronés : l’action se déclenche à contretemps, ou pire, ne se déclenche jamais alors que l’utilisateur la demandait clairement. Dites explicitement ce que l’action fait et ce qu’elle ne fait pas, et énoncez ce qu’elle retourne.
// Mauvais — trop vague
description: "Cherche des trucs"
// Bon — précis et contextuel
description: "Recherche des produits dans le catalogue e-commerce par mot-clé. Supporte le filtrage par catégorie. Retourne le nom, prix et disponibilité."
Appels HTTP vers votre API
La plupart des actions se contentent d’appeler le backend que vous exploitez déjà. L’exemple suivant crée une commande. Le second argument du handler, context, est la pièce importante : il porte l’identité de l’utilisateur connecté et son jeton d’accès, que vous transmettez tel quel dans l’en-tête Authorization. Vous n’avez donc jamais à demander à l’utilisateur qui il est — ChatGPT le sait déjà.
export const createOrder = defineAction({
name: "createOrder",
description: "Crée une commande pour les produits sélectionnés",
parameters: {
items: {
type: "array",
items: {
type: "object",
properties: {
productId: { type: "string" },
quantity: { type: "number" },
},
},
required: true,
},
shippingAddress: {
type: "string",
description: "Adresse de livraison complète",
required: true,
},
},
handler: async ({ items, shippingAddress }, context) => {
const userId = context.user.id;
const response = await fetch("https://api.monsite.com/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${context.auth.accessToken}`,
},
body: JSON.stringify({ userId, items, shippingAddress }),
});
if (!response.ok) {
throw new ActionError("ORDER_FAILED", "Impossible de créer la commande");
}
const order = await response.json();
return {
orderId: order.id,
total: order.total,
estimatedDelivery: order.deliveryDate,
};
},
});
Gestion des erreurs
Une erreur mal remontée devient, dans la conversation, un message incompréhensible pour l’utilisateur. Le modèle ne devine pas la cause d’un échec : il lit le code et le message que vous lui envoyez. D’où la règle — toute erreur prévisible mérite son ActionError explicite, et seule l’imprévisible tombe dans le filet générique.
import { ActionError } from "@openai/apps-sdk";
handler: async ({ productId }) => {
try {
const product = await fetchProduct(productId);
if (!product) {
throw new ActionError(
"NOT_FOUND",
`Le produit ${productId} est introuvable dans notre catalogue`
);
}
if (!product.inStock) {
throw new ActionError(
"OUT_OF_STOCK",
`${product.name} est actuellement en rupture de stock`
);
}
return product;
} catch (error) {
if (error instanceof ActionError) throw error;
throw new ActionError("INTERNAL", "Erreur technique, réessayez plus tard");
}
}
Chaque code déclenche un comportement différent du modèle, ce qui explique pourquoi le choix du code n’est pas cosmétique.
| Code | Usage | Comportement du modèle |
|---|---|---|
NOT_FOUND | Ressource introuvable | Suggère des alternatives |
AUTH_REQUIRED | Authentification nécessaire | Déclenche le flux OAuth |
RATE_LIMITED | Trop de requêtes | Informe et attend |
INTERNAL | Erreur serveur | Message générique |
Renvoyer INTERNAL là où AUTH_REQUIRED s’imposait, par exemple, prive l’utilisateur du flux OAuth qui aurait résolu son problème en deux clics ; il ne verra qu’un message d’excuse.
Actions composées
Rien ne vous oblige à découper votre logique en une action par appel réseau. Quand plusieurs opérations forment un tout du point de vue de l’utilisateur, regroupez-les : le modèle n’a plus qu’une décision à prendre et la latence perçue diminue. Le passage en caisse ci-dessous récupère le panier, contrôle le stock article par article, puis calcule le total avec promotions — et sort proprement en cas d’indisponibilité, sans faire échouer la conversation.
export const checkoutFlow = defineAction({
name: "checkout",
description: "Valide le panier, vérifie le stock et crée la commande",
parameters: {
cartId: { type: "string", required: true },
},
handler: async ({ cartId }, context) => {
// Étape 1 : Récupérer le panier
const cart = await getCart(cartId);
// Étape 2 : Vérifier le stock pour chaque article
const stockCheck = await Promise.all(
cart.items.map((item) => checkStock(item.productId, item.quantity))
);
const outOfStock = stockCheck.filter((s) => !s.available);
if (outOfStock.length > 0) {
return {
status: "stock_issue",
unavailable: outOfStock.map((s) => s.productName),
};
}
// Étape 3 : Calculer le total avec promotions
const total = await calculateTotal(cart, context.user.id);
return {
status: "ready",
items: cart.items.length,
total: total.amount,
currency: "EUR",
};
},
});
Remarquez que l’indisponibilité n’y est pas traitée comme une erreur mais comme un statut de retour : le modèle pourra annoncer quels articles manquent et proposer de poursuivre sans eux, ce qu’un ActionError aurait rendu impossible.
Pour ancrer tout cela, écrivez maintenant une action getUserProfile. Elle reçoit un userId en paramètre, appelle votre API pour récupérer le profil, retourne le nom, l’email et la date d’inscription, et signale par une ActionError le cas où l’utilisateur est introuvable. Relisez ensuite la description que vous avez rédigée : un lecteur qui ne connaîtrait pas votre code devrait comprendre en une phrase quand elle doit se déclencher.
Points clés à retenir
- La description de l’action guide le modèle — soyez précis
- Les paramètres utilisent un schema JSON typé avec validation automatique
- Utilisez
ActionErrorpour des erreurs explicites que le modèle peut interpréter - Le
contextfournit l’identité de l’utilisateur et les tokens d’authentification - Les actions composées permettent de chaîner plusieurs opérations en une seule étape