CodeGym /Courses /ChatGPT Apps /Multi‑App scenarios and App composition

Multi‑App scenarios and App composition

ChatGPT Apps
Level 20 , Lesson 4
Available

1. What Multi‑App scenarios are and why you need them

So far, we’ve looked at GiftGenius as the only external app in a given chat: the user picks your App from the list, ChatGPT loads your tools, and you’re the “main character” of the story. In the real Store it’s different: a user can connect multiple Apps at once, and ChatGPT will decide which specific app to call in response to a particular request.

For example, a single chat may have:

  • a corporate calendar app that knows colleagues’ birthdays;
  • GiftGenius, which selects gift ideas;
  • a company commerce app that can place orders and take payments.

The user writes: “Remind me about colleagues’ birthdays and immediately suggest what to gift and how to buy.” The model may sequentially call three different Apps: one for the calendar, a second for gift ideas, and a third for checkout.

It’s important to understand: the user doesn’t have a button “please call App #2 and here is its HTTP endpoint.” They communicate in natural language, and ChatGPT acts as the router — it reads the descriptions and metadata of all available apps and decides whom to call and when.

Three key takeaways follow:

  • There’s competition for context. Your App must be selected among dozens of others based on descriptions, names, and behavior.
  • Metadata becomes your “LLM SEO” — it determines whether the model will surface GiftGenius at the right moment or ignore it.
  • Think about interoperability: your outputs should be useful not only to the human in the chat but also to other Apps reading the same context.

Essentially, we turn an isolated ChatGPT App into a component of a larger system.

2. How the model chooses an App: a mental model of routing

Routing in Multi‑App scenarios works roughly like this (highly simplified, but useful for development):

  1. ChatGPT has a list of available Apps and their tools with metadata (name, description, parameters’ JSON Schema, annotations, and _meta).
  2. The user writes a message.
  3. The model forms an internal representation of intent and essentially performs semantic search over tools’ and apps’ descriptions to understand which tools are relevant.
  4. If the criteria match, it calls a tool or suggests opening an App.

There’s an important nuance: descriptions should be sufficiently distinct (discriminative). The wording “Product search” is not much different from “Gift search” or “Book search,” whereas “Gift idea search based on GiftGenius partner database” narrows the domain and raises the odds that your tool will be chosen for gift‑related queries.

A second detail — avoid name collisions. A tool named get_data says nothing in a world of dozens of Apps, while giftgenius_get_gift_catalog is much clearer. Especially in combination with a crisp description.

Finally, the model relies on context: if the chat already mentioned “gifts,” “birthdays,” and even the name GiftGenius, that highlights your App in the router’s eyes.

3. Metadata and descriptions as LLM SEO

Instead of treating metadata as “mandatory JSON boilerplate,” it’s helpful to think of it as product copywriting. Official recommendations explicitly say: treat metadata like product copy and design “one job per tool.”

Roughly, you can distinguish several levels of descriptions:

Level Audience What it describes
Manifest description Human + model The job of the entire App: why to include it in the chat
Tool description Model (routing) When to use a specific tool and for which tasks
Parameter descriptions Model (slot fill) How to fill arguments and what values are allowed
_meta["openai/widgetDescription"]
Model (UI) What exactly appears in the widget and whether to duplicate it in the model’s text reply

widgetDescription is especially important in a widget‑first world: the model doesn’t “see” your React code; it only knows which props you’ll pass and why. A well‑filled field prevents it from “making things up for you” and instead helps it adapt text responses based on what the UI already shows.

The Apps SDK docs emphasize: ChatGPT decides when and how to invoke your connector (App) based on metadata. Careful descriptions and parameter docs increase recall — the share of situations in which the model remembers your App at all — and reduce false positives.

Mini example: old vs. new description for GiftGenius

Suppose we previously had something like:

export const appDescription = `
GiftGenius — an assistant for finding and purchasing gifts.
`;

From a human perspective, that’s fine, but for routing in a Multi‑App world it’s better to emphasize when to use the App and what it doesn’t do:

export const appDescription = `
GiftGenius — a gift-idea assistant.
Use this app when the user asks to come up with a gift 
for a specific person or occasion and stay within a budget.
Do not use it for general online shopping or personal finance planning.
`;

Now it’s easier for the model to distinguish GiftGenius from a generic e‑commerce App or a financial advisor.

4. _meta["openai/widgetDescription"]: explain your UI to the model

In the table above we mentioned _meta["openai/widgetDescription"] separately. Let’s focus on this level: it helps the model “imagine” your widget and understand which parts of the answer are already covered by the UI and what should still be discussed in text.

Assume our main tool suggest_gifts returns a list of gifts, and the widget renders them as a horizontal carousel of cards. In the tool’s description we already explained when to use it, and in widgetDescription we explain what the result looks like.

A snippet of the tool descriptor (simplified, inspired by recommendations):

const suggestGiftsTool = {
  name: "suggest_gifts",
  description: "Use this to generate gift ideas within user's budget.",
  inputSchema: { /* ... */ },
  _meta: {
    "openai/widgetDescription":
      "Displays a horizontal list of gift cards with a price and a 'Buy' button. Do not repeat gift names in the text response."
  }
};

This achieves several goals at once:

  • The model knows the UI will already show names and prices — so the text reply can focus on explanations and advice rather than duplicating the list.
  • Other Apps (via the model) understand that toolOutput is not just a paragraph of text but a structured list that can be “picked up” into their context.

And yes, let’s be honest: writing such descriptions is less fun than coding, but they will save you hours of debugging odd model behavior later.

5. Tool annotations: readOnlyHint, destructiveHint, openWorldHint

In a Multi‑App world it’s important not only to decide “when to call,” but also how safe it is to call a particular tool. For this, the Apps SDK introduces a set of annotations in tool descriptors.

The idea is: annotations are soft hints to the model about the nature of the operation. They don’t replace server‑side authorization, but they strongly influence how ChatGPT will behave in chains.

A short summary (conceptual):

Annotation Meaning Typical model behavior
readOnlyHint
Does not change data Can be called frequently without extra confirmations
destructiveHint / isConsequential
Changes state (purchases, deletion) Ask the user for confirmation before calling
openWorldHint
Reaches into the “outside world” (search, web) The model is more cautious about volume and quality of the result

Annotations (readOnlyHint, destructiveHint, openWorldHint) are part of a standard tool description and can be used beyond ChatGPT. The field _meta["openai/isConsequential"] is a narrower, ChatGPT‑specific signal that further helps the model tell “safe” from “consequential” calls.

Let’s look at two GiftGenius tools:

  • suggest_gifts — catalog read, safe.
  • create_checkout_session — creates a checkout session, a clear side‑effect action.

Tool description example: suggest_gifts

const suggestGiftsTool = {
  name: "suggest_gifts",
  description:
    "Use this when the user asks for gift ideas for a person or occasion.",
  inputSchema: { /* ... */ },
  annotations: {
    readOnlyHint: true
  },
  _meta: {
    "openai/widgetDescription": "A carousel of gifts with price and a link.",
    "openai/isConsequential": false
  }
};

The model can call such a tool multiple times in a row, including “proactively,” to prepare options in advance without asking the user about each action.

Tool description example: create_checkout_session

const createCheckoutTool = {
  name: "create_checkout_session",
  description:
    "Finalize purchase of selected gifts via Instant Checkout.",
  inputSchema: { /* ... */ },
  annotations: {
    destructiveHint: true
  },
  _meta: {
    "openai/isConsequential": true
  }
};

Here we explicitly signal: this is a write operation, it has consequences (money charged, order created), and the model should request user confirmation before calling it, especially in long chains involving multiple Apps.

Don’t overestimate the magic: even with destructiveHint you must re‑validate inputs, tokens, and permissions on the server, as we discussed in the modules on security and authorization. But for Multi‑App orchestration, annotations help the model avoid “firing” such tools unnecessarily.

6. Isolated App vs. ecosystem: refining GiftGenius boundaries

When GiftGenius was the only App in a chat, you could afford a fairly broad scope: picking gifts, wrapping tips, holiday reminders, even short congratulation messages. The model would still call only your tools.

In a Multi‑App scenario, such a “we do everything” approach starts to hurt:

  • the router struggles to tell when you’re the perfect candidate and when another App is better suited;
  • you overlap with the calendar, a general task manager, a financial planner, and so on;
  • when multiple apps are used together, the model may pick the “wrong” performer and get confused by tools.

The best approach is to draw crisp boundaries:

  • GiftGenius: gift ideas only + purchase assistance via ACP/Checkout;
  • CalendarApp: events and reminders;
  • Finance app: the user’s overall budget, personal financial plan.

In App and tool descriptions, it’s useful to state not only “Use this when…” but also “Do not use when…”. The official discovery playbook recommends exactly this.

Mini example of a tool description:

description: `
Use this tool when the user explicitly asks for gift suggestions.
Do not use for generic product discovery or price comparison.
`

Such constraints not only help routing but also make your App’s behavior more predictable for product and QA.

7. App composition patterns: pipeline, handoff, shared context

In Multi‑App scenarios, three related ideas tend to surface in practice:

  • pipeline — several Apps run one after another (calendar → gifts → commerce), each doing its step;
  • handoff — one App’s output becomes the input to the next;
  • shared context — all of this handoff happens via the shared text context of the chat, with no direct HTTP calls between apps.

As we hinted, a Multi‑App scenario is not “App A calls App B over HTTP.” In the current ChatGPT Apps implementation, isolation is fairly strict: apps don’t call each other directly; communication goes through the shared text context.

We can phrase the basic pattern as follows:

  1. App A returns text or JSON to the chat (often inside structuredContent/a widget).
  2. The model reads that output.
  3. On the next turn, it may call App B, substituting details from A’s answer into arguments of B’s tools.

This is called a text/context handoff: “App A output → model → App B input.”

Example: CalendarApp + GiftGenius + CommerceApp

Let’s break down a concrete scenario.

User: “My boss’s birthday is tomorrow; pick a gift and purchase it right away.”

Step by step:

  1. The model understands it first needs the date and the person. It calls the calendar app’s tool — say, corporate_calendar.list_upcoming_birthdays — and gets a structure:

    [
      { "name": "Aleksey Bykov", "date": "2025-11-22", "relation": "manager" }
    ]
    
  2. Next the model decides it’s time to call GiftGenius. It calls your suggest_gifts with arguments obtained from the calendar:

    {
      "recipientName": "Aleksey",
      "occasion": "birthday",
      "budget": 150,
      "relationship": "manager"
    }
    

    The GiftGenius widget shows a gift carousel, and the text reply explains why these ideas fit.

  3. The user picks one or two options (via a button in the widget → widgetState), and the model then calls the commerce app’s tool — for example, corp_checkout.create_gift_order — with the selected SKU IDs and the shipping address.

From ChatGPT’s perspective, these are three different apps, but to the user it’s one conversation. The key for making this work:

  • crisp tool descriptions for each App;
  • careful naming (corporate_calendar.list_upcoming_birthdays, not just list_events);
  • a consistent format for structured data (so a gift idea is described in a way the commerce app can understand).

Visual diagram

You can depict this pipeline as:

sequenceDiagram
    participant U as User
    participant C as ChatGPT (Router)
    participant Cal as CalendarApp
    participant G as GiftGenius
    participant Com as CommerceApp

    U->>C: My boss's birthday is tomorrow — pick and purchase a gift
    C->>Cal: tools.call(list_upcoming_birthdays)
    Cal-->>C: [{ name, date, relation }]
    C->>G: tools.call(suggest_gifts, { recipient, occasion, budget })
    G-->>C: gift suggestions (+ widget)
    C-->>U: Explanation + GiftGenius widget
    U->>C: I like option #2 — buy it
    C->>Com: tools.call(create_gift_order, { skuId, address })
    Com-->>C: Order confirmation
    C-->>U: Done — the order has been placed

Your job as the GiftGenius developer is to make your voice clear and on point in this chorus, rather than interfering with others.

8. Interoperability: make outputs consumable by other Apps

In a Multi‑App world, it’s not enough to “answer nicely for the user.” Ideally, your toolOutput should be machine‑processable by another application: a commerce app, an analytics agent, a workflow orchestrator, etc.

This implies a couple of practical things:

  • use structured JSON in tool outputs rather than serialized “human” text;
  • stick to stable, understandable fields.

For example, you can type the result of suggest_gifts like this:

export type GiftSuggestion = {
  id: string;
  title: string;
  description: string;
  price: number;
  currency: string;
  forPerson: string;
  occasion: string;
  purchaseUrl: string;
};

And return an array of such objects in the tool response:

{
  "gifts": [
    {
      "id": "sku_123",
      "title": "Desktop planetarium",
      "description": "Mini starry-sky projector...",
      "price": 89.99,
      "currency": "USD",
      "forPerson": "Aleksey",
      "occasion": "birthday",
      "purchaseUrl": "https://shop.example.com/sku_123"
    }
  ]
}

The GiftGenius widget will take this as props and render cards nicely, and the commerce app, seeing this JSON in context, can pick up id and purchaseUrl for further checkout.

Practice shows that in Multi‑App scenarios, a good App returns data so another App can “consume” it — not just a human’s eyes.

9. Practical refactor of GiftGenius for Multi‑App

Let’s boil everything down to a few concrete changes in our teaching app.

Refine the manifest description

Suppose we have openai-app.json (or equivalent in a Next.js template) with:

{
  "name": "GiftGenius",
  "description": "Gift assistant for finding and buying presents."
}

Make it more explicit for routing:

{
  "name": "GiftGenius",
  "description": "Assistant for gift ideas and purchase flows. Use this app when the user asks what to gift a specific person or for a specific occasion within a budget. Do not use for generic online shopping or personal finance planning."
}

Now it’s clear this is not general shopping, not a financial advisor, and not a calendar.

Rewrite tool descriptions

The gift search tool:

const suggestGiftsTool = {
  name: "giftgenius_suggest_gifts",
  description: `
Use this when the user asks for gift ideas for a specific person or group,
optionally with a budget or occasion.
Do not use for non-gift product recommendations or travel booking.
`
};

The tool that pulls detailed SKU info from your catalog (read‑only):

const getGiftDetailsTool = {
  name: "giftgenius_get_gift_details",
  description: `
Use this to fetch more details for a gift suggested earlier by GiftGenius,
for example when the user asks “tell me more about option #2”.
`,
  annotations: { readOnlyHint: true }
};

The purchase tool — with destructiveHint, as shown earlier.

Update _meta["openai/widgetDescription"]

Assume our widget already has cards with a “Buy” CTA. Let’s hint this to the model:

const giftWidgetMeta = {
  _meta: {
    "openai/widgetDescription": `
Displays a list of gift cards with description, price, and a 'Buy' button.
The model should not repeat the full list in text — just comment on the choices and help the user decide.
`
  }
};

Now the model will be less likely to output a wall of ten gift names in the chat if they’re visible in the widget anyway, and will focus on explanations and logic — which is good for both UX and token cost.

10. A Multi‑App mindset for your future product

It’s important to switch from “how do I beat all competitors and be the user’s only App” to “how do I make my App an ideal module in a large ecosystem.”

This approach yields several practical benefits:

  • it’s easier to explain to users and Store reviewers why your App exists and when it’s appropriate;
  • it’s easier for the model to make routing decisions: less confusion, fewer “wrong” calls;
  • you’ll be able to design compositions deliberately: today with a calendar and commerce, tomorrow with a corporate HR bot or internal CRM.

Official discovery guides emphasize: design “one job per tool,” and treat metadata as a living artifact you test and update — not a static text from the first commit.

What you’ve already done in the earlier topics of Module 20 will help a lot here: golden cases, LLM evals, CI runs. You can extend the set of cases with scenarios like “the chat has both GiftGenius and CalendarApp,” and track how changes in descriptions affect App selection and answer quality.

11. Common mistakes when working with Multi‑App and composition

Mistake #1: an App description that says “I can do everything.”
If you write something like “a smart assistant for any task” in the manifest description, you’re competing not only with other Apps but also with baseline ChatGPT. The router struggles to understand when it must call you and when built‑in capabilities suffice. In a Multi‑App world, apps with a clear, narrow purpose win: “gift selection,” “calendar management,” “log analysis.”

Mistake #2: fuzzy tool descriptions and name collisions.
Tools named get_data, process_request with explanations like “processes user data” are great at confusing the model. In a multi‑app world, it’s easy to end up with your tool being called in the wrong domain. The right way is to bind domain and action (giftgenius_get_gift_catalog, calendar.list_birthdays) and explicitly state “Use this when… / Do not use when…”.

Mistake #3: ignoring _meta["openai/widgetDescription"].
Developers often fill only description and at best remember _meta for locale. As a result, the model doesn’t understand what the widget actually shows and either duplicates the UI in text or, conversely, promises a “table with prices” your widget doesn’t have. A couple of lines in widgetDescription save many such misunderstandings.

Mistake #4: missing readOnlyHint/destructiveHint annotations.
If all your tools look equally “neutral,” the model can’t tell which are safe to call frequently and which require user confirmation. In multi‑step scenarios with multiple Apps, this is especially critical: you might accidentally perform several write operations in a row without explicit human involvement. Remember to mark read‑only tools and explicitly highlight consequential/destructive actions.

Mistake #5: answers designed only for humans, not for other Apps.
Returning from a tool a “list of gifts” as a single line of text is tempting, but then any other App will struggle to use the result. Structured JSON with clear fields (id, price, currency, purchaseUrl, occasion) gives you an advantage in both UI and composition: the model can plug this data into other tools’ arguments without parsing natural language.

Mistake #6: trying to implement everything the user might need inside a single App.
It’s tempting: “since I’ve already built GiftGenius, let it also manage the calendar, send colleague emails, and plan the budget.” In an isolated world that’s tolerable, but in a Multi‑App context you become a kitchen‑sink app that conflicts with other narrow, well‑tuned Apps. It’s better to agree with yourself: my App does X and does it perfectly; the rest is someone else’s responsibility. This design greatly simplifies both UX and ecosystem growth.

Mistake #7: not testing behavior in an environment with other Apps.
Developers often test their app in Dev Mode in a “clean” chat where there are no other Apps. But in the Store, a user may have a dozen connected apps, some overlapping conceptually with yours. Don’t be lazy — create a test scenario where the chat includes neighboring Apps (calendar, general shopping, finance), and run golden cases: does the model correctly select GiftGenius for gift‑related queries, and does it avoid confusing it with other participants?

1
Task
ChatGPT Apps, level 20, lesson 4
Locked
WidgetDescription as “instructions for the router and UI”
WidgetDescription as “instructions for the router and UI”
1
Task
ChatGPT Apps, level 20, lesson 4
Locked
Namespacing tools + discriminative descriptions (anti-collisions)
Namespacing tools + discriminative descriptions (anti-collisions)
1
Survey/quiz
LLM-Apps: the next generation, level 20, lesson 4
Unavailable
LLM-Apps: the next generation
LLM-Apps: the next generation
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION